Compare commits
333 Commits
pivx
...
mweb_uncon
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6e7d36e84 | ||
|
|
6e4feb33d7 | ||
|
|
1f810fab6b | ||
|
|
7bf5f7ddfa | ||
|
|
d7da532111 | ||
|
|
fa35102794 | ||
|
|
da8c3e0237 | ||
|
|
ec29c9889c | ||
|
|
14298d022a | ||
|
|
5ceaab57d1 | ||
|
|
a5c3534c19 | ||
|
|
3b6f72c084 | ||
|
|
7c0ea36e37 | ||
|
|
14577f7741 | ||
|
|
dbf3f8f034 | ||
|
|
b85d234a0b | ||
|
|
1bfb271b87 | ||
|
|
f9bc5d46af | ||
|
|
fab89a42f3 | ||
|
|
3e14a784f3 | ||
|
|
e4f196411a | ||
|
|
8318961f0b | ||
|
|
7c9504e0cd | ||
|
|
98f3a52daa | ||
|
|
3276f9abd4 | ||
|
|
041ab18288 | ||
|
|
d57366c0b2 | ||
|
|
1ec1764012 | ||
|
|
28fc4817c0 | ||
|
|
2d1bd87b41 | ||
|
|
9ee6669179 | ||
|
|
30a5ea1652 | ||
|
|
53ceae718b | ||
|
|
1af9f64020 | ||
|
|
cfd2151c1a | ||
|
|
237d12afa0 | ||
|
|
a0cdd8cec9 | ||
|
|
1754650e75 | ||
|
|
3b8b512003 | ||
|
|
81649dcf9b | ||
|
|
f5d4b8dc0d | ||
|
|
bcfd63037a | ||
|
|
ddf3734f9d | ||
|
|
8c07ee5108 | ||
|
|
6ad5880ba4 | ||
|
|
0ff0a13a67 | ||
|
|
ff7e8fe0aa | ||
|
|
1068694990 | ||
|
|
66d1abd888 | ||
|
|
671e626551 | ||
|
|
192aff221e | ||
|
|
03fdf44220 | ||
|
|
38fa498b0b | ||
|
|
7547587d4e | ||
|
|
fd0772f893 | ||
|
|
0a12625290 | ||
|
|
d6ed5ba24c | ||
|
|
fb48797298 | ||
|
|
5bec1c31da | ||
|
|
3d3fcbde0b | ||
|
|
61845a7a84 | ||
|
|
65c93eaee6 | ||
|
|
6a26f72bca | ||
|
|
0a9db22828 | ||
|
|
08f0156b75 | ||
|
|
15bf9b2187 | ||
|
|
258b730c41 | ||
|
|
b409fe9f0e | ||
|
|
01bb3870b6 | ||
|
|
9efb244952 | ||
|
|
0be5a4fca7 | ||
|
|
a4c79fb7aa | ||
|
|
5b6f447692 | ||
|
|
28baa597cc | ||
|
|
d4a6ad7d6f | ||
|
|
ce578f8025 | ||
|
|
c387bfec71 | ||
|
|
d668a2f342 | ||
|
|
8e17ee5939 | ||
|
|
3b55d17a26 | ||
|
|
fd0bf9ed73 | ||
|
|
e6c7c4d9bb | ||
|
|
7053d7ee4b | ||
|
|
22cd3cf9f1 | ||
|
|
05e6edd5df | ||
|
|
9a1b7db2dc | ||
|
|
2a2f1ca3b6 | ||
|
|
db0e85d37c | ||
|
|
ebcd7738e5 | ||
|
|
4d8d421de6 | ||
|
|
23330c20bc | ||
|
|
22e005728a | ||
|
|
f2b69e5498 | ||
|
|
d617ab1d6b | ||
|
|
e7ae290eb5 | ||
|
|
20b405a944 | ||
|
|
c01b4a3d70 | ||
|
|
7caac8a8eb | ||
|
|
a17129999c | ||
|
|
5775ac5931 | ||
|
|
0b963bffde | ||
|
|
45e49848b1 | ||
|
|
5c23983c8e | ||
|
|
68ff57ebdc | ||
|
|
7fd60b3e82 | ||
|
|
7735c9733a | ||
|
|
af876fa166 | ||
|
|
8f4b962285 | ||
|
|
a13a5d4bf6 | ||
|
|
547e50acb6 | ||
|
|
55ded71686 | ||
|
|
c3b33c502e | ||
|
|
67624a252b | ||
|
|
d4f6286980 | ||
|
|
0432fae5b5 | ||
|
|
9888c4ebe1 | ||
|
|
7bc5fc78ba | ||
|
|
705ac2c6fc | ||
|
|
303499fc6f | ||
|
|
724f9348d5 | ||
|
|
4464ca1746 | ||
|
|
08d3f05c1c | ||
|
|
be46d8a7bd | ||
|
|
f6fb11f452 | ||
|
|
00bebfa371 | ||
|
|
eb44dc9626 | ||
|
|
1859eccf0e | ||
|
|
416b003da9 | ||
|
|
88fd8ac23e | ||
|
|
04d4d89e96 | ||
|
|
57554d4fec | ||
|
|
cbb3d0ac02 | ||
|
|
1dcd750fea | ||
|
|
75c5f6a905 | ||
|
|
9645e87961 | ||
|
|
2b2b98505b | ||
|
|
1093eaed3b | ||
|
|
788efeef5b | ||
|
|
f02b62b6ea | ||
|
|
8885a58a2e | ||
|
|
0041fb4a3c | ||
|
|
81e7825b51 | ||
|
|
5c0e70c5b2 | ||
|
|
5309ff1b50 | ||
|
|
e67ca94dfb | ||
|
|
5536c82ded | ||
|
|
5b6c8a23a9 | ||
|
|
87937ec0ac | ||
|
|
c5ab11d015 | ||
|
|
3a97c0c7bb | ||
|
|
90aaa46918 | ||
|
|
697d88d5f2 | ||
|
|
6e94c4a05d | ||
|
|
409c75851f | ||
|
|
22576c0316 | ||
|
|
b5a4df9908 | ||
|
|
ca3bfe858c | ||
|
|
37952fd296 | ||
|
|
159d3e2c33 | ||
|
|
89e0080173 | ||
|
|
484d811fe7 | ||
|
|
ea8cc70696 | ||
|
|
97506850c4 | ||
|
|
724e7f0ffc | ||
|
|
f33629f2a5 | ||
|
|
d8de9a6aa7 | ||
|
|
114e8e4d2b | ||
|
|
0a2133f43f | ||
|
|
06065958b7 | ||
|
|
09cc523ac3 | ||
|
|
5b0c1e9b51 | ||
|
|
577849f01c | ||
|
|
6ccfd93997 | ||
|
|
b6046fdbf3 | ||
|
|
c2d6cdafdd | ||
|
|
c322c9ae0c | ||
|
|
c13606ab54 | ||
|
|
c5bd58afc2 | ||
|
|
2922b171a6 | ||
|
|
3234e3fba3 | ||
|
|
ac16fc07a4 | ||
|
|
3241616d68 | ||
|
|
dc0bd147b8 | ||
|
|
9117e2b723 | ||
|
|
c9e04332fc | ||
|
|
a2fa2ff9de | ||
|
|
83a077e5b0 | ||
|
|
90ce6b3e04 | ||
|
|
1a136cd219 | ||
|
|
553af1a3e8 | ||
|
|
9729dcf526 | ||
|
|
ef71ca7ef4 | ||
|
|
149616a59f | ||
|
|
9677c48f39 | ||
|
|
ac10c9db76 | ||
|
|
c4321b7740 | ||
|
|
2a3d89b112 | ||
|
|
fb5e8ff8b1 | ||
|
|
ac07727da7 | ||
|
|
09ce086790 | ||
|
|
80a78b4070 | ||
|
|
5ce178e673 | ||
|
|
ef6653a8db | ||
|
|
3f71dffe5a | ||
|
|
2a9e423eaa | ||
|
|
6860279faa | ||
|
|
80df3b1a34 | ||
|
|
efcee68663 | ||
|
|
31aaacf4e1 | ||
|
|
7101a5d1ee | ||
|
|
ef51719e62 | ||
|
|
0e1cb6d03d | ||
|
|
1d0a3fbc12 | ||
|
|
55f6095ec2 | ||
|
|
7d43512845 | ||
|
|
c90fa6f2c6 | ||
|
|
23e89882a4 | ||
|
|
a250daca8b | ||
|
|
789cd0f6ab | ||
|
|
2a35148a4b | ||
|
|
9370eff4a6 | ||
|
|
b5b43a8bf3 | ||
|
|
7bc411eb98 | ||
|
|
47c1237f6d | ||
|
|
d15cf3dd6f | ||
|
|
ba5339d8bd | ||
|
|
6e5c54b447 | ||
|
|
391f6ffe80 | ||
|
|
c5f31f0d1e | ||
|
|
0f7df9e5f1 | ||
|
|
aabf865873 | ||
|
|
1bcce46a7c | ||
|
|
cb6e773848 | ||
|
|
640d22bdc5 | ||
|
|
e5749a397f | ||
|
|
a520ebf23f | ||
|
|
b43d58afbf | ||
|
|
59adf3368b | ||
|
|
d2de181550 | ||
|
|
0bd75fae0e | ||
|
|
a6162718c8 | ||
|
|
850f2c7b17 | ||
|
|
2ba2ff5791 | ||
|
|
5d4db2c1df | ||
|
|
8022ada01f | ||
|
|
d08e85e73e | ||
|
|
8ec6d55963 | ||
|
|
54e434e1c9 | ||
|
|
bbe7556d18 | ||
|
|
093a36c8d2 | ||
|
|
fc31615a97 | ||
|
|
020a65db8a | ||
|
|
d78ff8573c | ||
|
|
09394c58a6 | ||
|
|
eb8aa18224 | ||
|
|
e525878aef | ||
|
|
601d469801 | ||
|
|
f65ae07cf9 | ||
|
|
ae2ddc4049 | ||
|
|
8b2d2b446b | ||
|
|
5a73fab045 | ||
|
|
c440f9e3a3 | ||
|
|
ca264db0d0 | ||
|
|
c0c2c8b9bb | ||
|
|
515e6655e8 | ||
|
|
f210024e93 | ||
|
|
45d6b9ecbf | ||
|
|
50ed1bfccf | ||
|
|
18974d9458 | ||
|
|
65183133d8 | ||
|
|
9da907aa1e | ||
|
|
59fb6c18ed | ||
|
|
e46fd193fe | ||
|
|
4fc68b5717 | ||
|
|
9557da8714 | ||
|
|
a9f3eeefff | ||
|
|
3c2eb5f9f8 | ||
|
|
5701f6e830 | ||
|
|
aa14da27af | ||
|
|
4866ff4db8 | ||
|
|
7298867e18 | ||
|
|
9fc4fe3b79 | ||
|
|
468d8c5cc1 | ||
|
|
3c84f0230e | ||
|
|
842027f114 | ||
|
|
6ae8651d1b | ||
|
|
eb9eb49bd9 | ||
|
|
7e4dd125e1 | ||
|
|
18e89a3c44 | ||
|
|
3f628026ae | ||
|
|
e29e43821e | ||
|
|
c7bbdc7022 | ||
|
|
3ed6781c9f | ||
|
|
5f0e708812 | ||
|
|
9d8fe87fc3 | ||
|
|
58c9006a01 | ||
|
|
b0f6865c90 | ||
|
|
7d0cd6d08d | ||
|
|
ea52ff67c2 | ||
|
|
d6e0b4d2b2 | ||
|
|
bdcf6ce91b | ||
|
|
9e042200f3 | ||
|
|
a6a6865e95 | ||
|
|
1dc626e305 | ||
|
|
c87f66a041 | ||
|
|
c1482a8280 | ||
|
|
1987931332 | ||
|
|
e0b575b50e | ||
|
|
5738cdd825 | ||
|
|
58a2707526 | ||
|
|
e913957e41 | ||
|
|
0472958fc2 | ||
|
|
1ea73a7917 | ||
|
|
9102efecb6 | ||
|
|
a040a6172a | ||
|
|
9495249e70 | ||
|
|
af766876a1 | ||
|
|
7319d46988 | ||
|
|
c4457ca400 | ||
|
|
a4f4a411a9 | ||
|
|
f29eac0eb5 | ||
|
|
eb30369bd4 | ||
|
|
ef0f5ea1ea | ||
|
|
46d1856f71 | ||
|
|
6b4666d632 | ||
|
|
afba673085 | ||
|
|
1694e73f92 | ||
|
|
146698425d | ||
|
|
b89f94f0cc | ||
|
|
b5a009d3f9 | ||
|
|
d74699992b | ||
|
|
cacd29130e | ||
|
|
e4e35b0d89 |
@@ -7,8 +7,8 @@ lint_task:
|
|||||||
- pip install codespell
|
- pip install codespell
|
||||||
script:
|
script:
|
||||||
- flake8 --version
|
- flake8 --version
|
||||||
- PYTHONWARNINGS="ignore" flake8 --ignore=E501,F841,W503 --exclude=basicswap/contrib,messages_pb2.py,.eggs,.tox,bin/install_certifi.py
|
- 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,*mnemonics.py,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
|
||||||
|
|
||||||
test_task:
|
test_task:
|
||||||
environment:
|
environment:
|
||||||
@@ -24,7 +24,7 @@ test_task:
|
|||||||
- apt-get install -y wget python3-pip gnupg unzip protobuf-compiler automake libtool pkg-config
|
- apt-get install -y wget python3-pip gnupg unzip protobuf-compiler automake libtool pkg-config
|
||||||
- pip install tox pytest
|
- pip install tox pytest
|
||||||
- python3 setup.py install
|
- python3 setup.py install
|
||||||
- wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_v0.1.zip
|
- wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_v0.2.zip
|
||||||
- unzip -d coincurve-anonswap coincurve-anonswap.zip
|
- unzip -d coincurve-anonswap coincurve-anonswap.zip
|
||||||
- mv ./coincurve-anonswap/*/{.,}* ./coincurve-anonswap || true
|
- mv ./coincurve-anonswap/*/{.,}* ./coincurve-anonswap || true
|
||||||
- cd coincurve-anonswap
|
- cd coincurve-anonswap
|
||||||
@@ -47,3 +47,4 @@ test_task:
|
|||||||
- pytest tests/basicswap/test_other.py
|
- pytest tests/basicswap/test_other.py
|
||||||
- pytest tests/basicswap/test_run.py
|
- pytest tests/basicswap/test_run.py
|
||||||
- pytest tests/basicswap/test_reload.py
|
- pytest tests/basicswap/test_reload.py
|
||||||
|
- pytest tests/basicswap/test_btc_xmr.py -k 'test_01_a or test_01_b or test_02_a or test_02_b'
|
||||||
|
|||||||
BIN
.github-readme/basicswap_header.jpg
Normal file
|
After Width: | Height: | Size: 230 KiB |
4
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
old/
|
old/
|
||||||
|
build/
|
||||||
*.pyc
|
*.pyc
|
||||||
__pycache__
|
__pycache__
|
||||||
/dist/
|
/dist/
|
||||||
@@ -8,3 +9,6 @@ __pycache__
|
|||||||
.tox
|
.tox
|
||||||
.eggs
|
.eggs
|
||||||
*~
|
*~
|
||||||
|
|
||||||
|
# geckodriver.log
|
||||||
|
*.log
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ before_install:
|
|||||||
install:
|
install:
|
||||||
- travis_retry pip install tox pytest
|
- travis_retry pip install tox pytest
|
||||||
before_script:
|
before_script:
|
||||||
- wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_v0.1.zip
|
- wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_v0.2.zip
|
||||||
- unzip -d coincurve-anonswap coincurve-anonswap.zip
|
- unzip -d coincurve-anonswap coincurve-anonswap.zip
|
||||||
- mv ./coincurve-anonswap/*/{.,}* ./coincurve-anonswap || true
|
- mv ./coincurve-anonswap/*/{.,}* ./coincurve-anonswap || true
|
||||||
- cd coincurve-anonswap
|
- cd coincurve-anonswap
|
||||||
@@ -52,8 +52,8 @@ jobs:
|
|||||||
- travis_retry pip install codespell==1.15.0
|
- travis_retry pip install codespell==1.15.0
|
||||||
before_script:
|
before_script:
|
||||||
script:
|
script:
|
||||||
- PYTHONWARNINGS="ignore" flake8 --ignore=E501,F841,W503 --exclude=basicswap/contrib,messages_pb2.py,.eggs,.tox,bin/install_certifi.py
|
- 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,*mnemonics.py,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:
|
after_success:
|
||||||
- echo "End lint"
|
- echo "End lint"
|
||||||
- stage: test
|
- stage: test
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ RUN wget -O protobuf_src.tar.gz https://github.com/protocolbuffers/protobuf/rele
|
|||||||
make -j$(nproc) install && \
|
make -j$(nproc) install && \
|
||||||
ldconfig
|
ldconfig
|
||||||
|
|
||||||
ARG COINCURVE_VERSION=v0.1
|
ARG COINCURVE_VERSION=v0.2
|
||||||
RUN wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_$COINCURVE_VERSION.zip && \
|
RUN wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_$COINCURVE_VERSION.zip && \
|
||||||
unzip coincurve-anonswap.zip && \
|
unzip coincurve-anonswap.zip && \
|
||||||
mv ./coincurve-anonswap_$COINCURVE_VERSION ./coincurve-anonswap && \
|
mv ./coincurve-anonswap_$COINCURVE_VERSION ./coincurve-anonswap && \
|
||||||
|
|||||||
2
LICENSE
@@ -1,5 +1,5 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
Copyright (c) 2019 tecnovert
|
Copyright (c) 2019-2024 tecnovert
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
135
README.md
@@ -1,20 +1,131 @@
|
|||||||
|
# BasicSwap DEX (BSX)
|
||||||
|
|
||||||
# Simple Atomic Swap Network - Proof of Concept
|

|
||||||
|
|
||||||
## Overview
|
**[Official Website](https://basicswapdex.com)** | **[News](https://particl.news)** | **[Tutorials](https://academy.particl.io)** | **[Chat]( https://matrix.to/#/#basicswap:matrix.org )**
|
||||||
|
|
||||||
Simple atomic swap experiment, doesn't have many interesting features yet.
|
Table of Contents
|
||||||
Not ready for real world use.
|
|
||||||
|
|
||||||
Uses Particl secure messaging and Decred style atomic swaps.
|
* [About](#about)
|
||||||
|
* [Features](#features)
|
||||||
|
* [Available Assets](#available-assets)
|
||||||
|
* [Participate](#participate)
|
||||||
|
* [Tutorials](#tutorials)
|
||||||
|
* [License](#license)
|
||||||
|
|
||||||
The Particl node is used to hold the keys and sign for the swap transactions.
|
## About
|
||||||
Other nodes can be run in pruned mode.
|
|
||||||
A node must be run for each coin type traded.
|
|
||||||
In the future it should be possible to use data from explorers instead of running a node.
|
|
||||||
|
|
||||||
## Currently a work in progress
|
The BasicSwap DEX is a privacy-first and decentralized exchange which features cross-chain atomic swaps and a distributed order book.
|
||||||
|
|
||||||
Not ready for real-world use.
|
[BasicSwap](https://academy.particl.io/en/latest/glossary.html#term-BasicSwap) is a cross-chain and privacy-centric DEX (decentralized exchange) that lets you trade cryptocurrencies with no third party involvement. Its distributed order book lets you make or take orders at no cost and trade within a free and open environment without central points of failure.
|
||||||
|
|
||||||
Discuss development and help with testing in the matrix channel [#basicswap:matrix.org](https://riot.im/app/#/room/#basicswap:matrix.org)
|
This DEX protocol was built in direct response to the increasingly invasive demands and data mining practices of today’s cryptocurrency exchanges. It strives to bring more decentralized and more private cryptocurrency trading conditions for all.
|
||||||
|
|
||||||
|
BasicSwap is still in beta. This means that, while it already offers most of the vital trading features you’d expect to see on centralized exchanges, it is still in heavy development, and many more features will come about in the near future.
|
||||||
|
|
||||||
|
Check out our [roadmap](https://basicswapdex.com/roadmap) to get a better idea of what we've got planned for it!
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* **True cross-chain support** — Swap cryptocurrencies that live on entirely different blockchain environments, like Bitcoin and Monero.
|
||||||
|
* **Distributed order book** — Make or take limit orders on a completely distributed order book system.
|
||||||
|
* **No third-party or middleman** — Trade crypto with no intermediaries whatsoever.
|
||||||
|
* **No trading fees** — Only pay the typical cryptocurrency network fee.
|
||||||
|
* **Privacy from the ground up** — Every component of BasicSwap is built with a privacy-first commitment.
|
||||||
|
* **Full Monero support** — Swap Monero with a variety of other cryptocurrencies like Bitcoin or Particl. No wrapped assets or trickery involved.
|
||||||
|
* **User-friendly interface** — Enjoy all these features within a user-friendly and intuitive interface that handles all the complicated parts for you.
|
||||||
|
|
||||||
|
BasicSwap is still in beta. This means that, while it already offers most of the vital trading features you’d expect to see on centralized exchanges, it is still in heavy development, and many more features will come about in the near future.
|
||||||
|
|
||||||
|
## Available Assets
|
||||||
|
|
||||||
|
BasicSwap is compatible with the following digital assets.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Coin Name</strong>
|
||||||
|
</td>
|
||||||
|
<td><strong>Ticker</strong>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Bitcoin
|
||||||
|
</td>
|
||||||
|
<td>BTC
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Monero
|
||||||
|
</td>
|
||||||
|
<td>XMR
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Dash
|
||||||
|
</td>
|
||||||
|
<td>DASH
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Litecoin
|
||||||
|
</td>
|
||||||
|
<td>LTC
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Firo
|
||||||
|
</td>
|
||||||
|
<td>FIRO
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>PIVX
|
||||||
|
</td>
|
||||||
|
<td>PIVX
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Particl
|
||||||
|
</td>
|
||||||
|
<td>PART
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
We plan on adding many other cryptocurrencies moving forward, including ETH and its ERC-20 tokens. However, due to the true cross-chain nature of the BasicSwap DEX protocol, each integration has to be done on a case-by-case basis.
|
||||||
|
|
||||||
|
If you’d like to add a cryptocurrency to BasicSwap, either [apply for a listing using our listing application form](https://forms.gle/9DsHoHTJVqSiMNHW9), or try coding the integration yourself by referencing how other cryptocurrencies have been added. Follow [this link](https://academy.particl.io/en/latest/basicswap-guides/basicswapguides_apply.html) for more information on how to integrate a coin yourself.
|
||||||
|
|
||||||
|
# Participate
|
||||||
|
|
||||||
|
### Chats
|
||||||
|
|
||||||
|
* **For developers** The chat [#particl-dev:matrix.org](https://app.element.io/#/room/#particl-dev:matrix.org) using [Element](https://element.io) (formerly Riot).
|
||||||
|
* **For community** The community chat [https://discord.me/particl](https://discord.me/particl) [](https://discord.me/particl).
|
||||||
|
|
||||||
|
[](http://twitter.com/BasicSwapDEX)
|
||||||
|
[](http://reddit.com/r/particl)
|
||||||
|
|
||||||
|
### Documentation, installation
|
||||||
|
|
||||||
|
For non-developers curious to explore a new world of commerce, binaries can be downloaded and installed. It is the easiest way to get started. Following the guides on [Particl Academy](https://academy.particl.io), a reference book in straightforward language, is recommended.
|
||||||
|
|
||||||
|
* [Download BasicSwapDEX](https://github.com/tecnovert/basicswap/tree/master/doc)
|
||||||
|
|
||||||
|
#### Community chat support
|
||||||
|
|
||||||
|
* [Discord](https://discord.me/particl) navigate to the #support channel
|
||||||
|
|
||||||
|
* [Telegram](https://t.me/particlhelp)
|
||||||
|
|
||||||
|
* [Element](https://app.element.io/#/room/#particlhelp:matrix.org)
|
||||||
|
|
||||||
|
# Tutorials
|
||||||
|
|
||||||
|
You can find a wide variety of tutorials and step-by-step guides about BasicSwap on the [Particl Academy](https://academy.particl.io) or on Particl’s Youtube channel.
|
||||||
|
|
||||||
|
If you encounter an issue or try to accomplish something not mentioned in any of the tutorials included in the links above, please join the community chat support channels; you’ll be sure to find help and support from our awesome community and open-source team there!
|
||||||
|
|
||||||
|
# License
|
||||||
|
|
||||||
|
BasicSwap is released under MIT software license.
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
name = "basicswap"
|
name = "basicswap"
|
||||||
|
|
||||||
__version__ = "0.0.36"
|
__version__ = "0.12.7"
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2019-2022 tecnovert
|
# Copyright (c) 2019-2024 tecnovert
|
||||||
# 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
|
||||||
|
import time
|
||||||
import shlex
|
import shlex
|
||||||
import socks
|
import socks
|
||||||
|
import random
|
||||||
import socket
|
import socket
|
||||||
import urllib
|
import urllib
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
|
import traceback
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
import basicswap.config as cfg
|
from sockshandler import SocksiPyHandler
|
||||||
import basicswap.contrib.segwit_addr as segwit_addr
|
|
||||||
|
|
||||||
from .rpc import (
|
from .rpc import (
|
||||||
callrpc,
|
callrpc,
|
||||||
@@ -36,8 +38,8 @@ class BaseApp:
|
|||||||
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.is_running = True
|
|
||||||
self.fail_code = 0
|
self.fail_code = 0
|
||||||
|
self.mock_time_offset = 0
|
||||||
|
|
||||||
self.data_dir = data_dir
|
self.data_dir = data_dir
|
||||||
self.chain = chain
|
self.chain = chain
|
||||||
@@ -47,6 +49,8 @@ class BaseApp:
|
|||||||
self.mxDB = threading.RLock()
|
self.mxDB = threading.RLock()
|
||||||
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._network = None
|
self._network = None
|
||||||
self.prepareLogging()
|
self.prepareLogging()
|
||||||
self.log.info('Network: {}'.format(self.chain))
|
self.log.info('Network: {}'.format(self.chain))
|
||||||
@@ -63,7 +67,7 @@ class BaseApp:
|
|||||||
def stopRunning(self, with_code=0):
|
def stopRunning(self, with_code=0):
|
||||||
self.fail_code = with_code
|
self.fail_code = with_code
|
||||||
with self.mxDB:
|
with self.mxDB:
|
||||||
self.is_running = False
|
self.chainstate_delay_event.set()
|
||||||
self.delay_event.set()
|
self.delay_event.set()
|
||||||
|
|
||||||
def prepareLogging(self):
|
def prepareLogging(self):
|
||||||
@@ -73,10 +77,10 @@ class BaseApp:
|
|||||||
# Remove any existing handlers
|
# Remove any existing handlers
|
||||||
self.log.handlers = []
|
self.log.handlers = []
|
||||||
|
|
||||||
formatter = logging.Formatter('%(asctime)s %(levelname)s : %(message)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'))
|
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,7 +96,7 @@ class BaseApp:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def setDaemonPID(self, name, pid):
|
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
|
||||||
@@ -100,23 +104,17 @@ class BaseApp:
|
|||||||
if v['name'] == name:
|
if v['name'] == name:
|
||||||
v['pid'] = pid
|
v['pid'] = pid
|
||||||
|
|
||||||
def getChainDatadirPath(self, coin):
|
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):
|
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 encodeSegwit(self, coin_type, raw):
|
|
||||||
return segwit_addr.encode(chainparams[coin_type][self.chain]['hrp'], 0, raw)
|
|
||||||
|
|
||||||
def decodeSegwit(self, coin_type, addr):
|
|
||||||
return bytes(segwit_addr.decode(chainparams[coin_type][self.chain]['hrp'], addr)[1])
|
|
||||||
|
|
||||||
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'])
|
||||||
@@ -125,18 +123,6 @@ class BaseApp:
|
|||||||
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 calltx(self, cmd):
|
|
||||||
bindir = self.coin_clients[Coins.PART]['bindir']
|
|
||||||
args = [os.path.join(bindir, cfg.PARTICL_TX), ]
|
|
||||||
if self.chain != 'mainnet':
|
|
||||||
args.append('-' + self.chain)
|
|
||||||
args += shlex.split(cmd)
|
|
||||||
p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
out = p.communicate()
|
|
||||||
if len(out[1]) > 0:
|
|
||||||
raise ValueError('TX error ' + str(out[1]))
|
|
||||||
return out[0].decode('utf-8').strip()
|
|
||||||
|
|
||||||
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']
|
||||||
@@ -152,7 +138,7 @@ class BaseApp:
|
|||||||
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):
|
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()
|
||||||
@@ -170,12 +156,26 @@ class BaseApp:
|
|||||||
|
|
||||||
socket.setdefaulttimeout(timeout)
|
socket.setdefaulttimeout(timeout)
|
||||||
|
|
||||||
def popConnectionParameters(self):
|
def popConnectionParameters(self) -> None:
|
||||||
if self.use_tor_proxy:
|
if self.use_tor_proxy:
|
||||||
socket.socket = self.default_socket
|
socket.socket = self.default_socket
|
||||||
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:
|
||||||
|
open_handler = None
|
||||||
|
if self.use_tor_proxy:
|
||||||
|
open_handler = SocksiPyHandler(socks.PROXY_TYPE_SOCKS5, self.tor_proxy_host, self.tor_proxy_port)
|
||||||
|
opener = urllib.request.build_opener(open_handler) if self.use_tor_proxy else urllib.request.build_opener()
|
||||||
|
opener.addheaders = [('User-agent', 'Mozilla/5.0')]
|
||||||
|
request = urllib.request.Request(url, headers=headers)
|
||||||
|
return opener.open(request, timeout=timeout).read()
|
||||||
|
|
||||||
|
def logException(self, message) -> None:
|
||||||
|
self.log.error(message)
|
||||||
|
if self.debug:
|
||||||
|
self.log.error(traceback.format_exc())
|
||||||
|
|
||||||
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')
|
||||||
@@ -192,3 +192,35 @@ class BaseApp:
|
|||||||
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:
|
||||||
|
return int(time.time()) + self.mock_time_offset
|
||||||
|
|
||||||
|
def setMockTimeOffset(self, new_offset: int) -> None:
|
||||||
|
self.log.warning(f'Setting mocktime to {new_offset}')
|
||||||
|
self.mock_time_offset = new_offset
|
||||||
|
|
||||||
|
def get_int_setting(self, name: str, default_v: int, min_v: int, max_v) -> int:
|
||||||
|
value: int = self.settings.get(name, default_v)
|
||||||
|
if value < min_v:
|
||||||
|
self.log.warning(f'Setting {name} to {min_v}')
|
||||||
|
value = min_v
|
||||||
|
if value > max_v:
|
||||||
|
self.log.warning(f'Setting {name} to {max_v}')
|
||||||
|
value = max_v
|
||||||
|
return value
|
||||||
|
|
||||||
|
def get_delay_event_seconds(self):
|
||||||
|
if self.min_delay_event == self.max_delay_event:
|
||||||
|
return self.min_delay_event
|
||||||
|
return random.randrange(self.min_delay_event, self.max_delay_event)
|
||||||
|
|
||||||
|
def get_short_delay_event_seconds(self):
|
||||||
|
if self.min_delay_event_short == self.max_delay_event_short:
|
||||||
|
return self.min_delay_event_short
|
||||||
|
return random.randrange(self.min_delay_event_short, self.max_delay_event_short)
|
||||||
|
|
||||||
|
def get_delay_retry_seconds(self):
|
||||||
|
if self.min_delay_retry == self.max_delay_retry:
|
||||||
|
return self.min_delay_retry
|
||||||
|
return random.randrange(self.min_delay_retry, self.max_delay_retry)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2021-2022 tecnovert
|
# Copyright (c) 2021-2023 tecnovert
|
||||||
# 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.
|
||||||
|
|
||||||
@@ -9,8 +9,8 @@ import struct
|
|||||||
import hashlib
|
import hashlib
|
||||||
from enum import IntEnum, auto
|
from enum import IntEnum, auto
|
||||||
from .util.address import (
|
from .util.address import (
|
||||||
decodeAddress,
|
|
||||||
encodeAddress,
|
encodeAddress,
|
||||||
|
decodeAddress,
|
||||||
)
|
)
|
||||||
from .chainparams import (
|
from .chainparams import (
|
||||||
chainparams,
|
chainparams,
|
||||||
@@ -47,6 +47,9 @@ class MessageTypes(IntEnum):
|
|||||||
XMR_BID_LOCK_RELEASE_LF = auto()
|
XMR_BID_LOCK_RELEASE_LF = auto()
|
||||||
OFFER_REVOKE = auto()
|
OFFER_REVOKE = auto()
|
||||||
|
|
||||||
|
ADS_BID_LF = auto()
|
||||||
|
ADS_BID_ACCEPT_FL = auto()
|
||||||
|
|
||||||
|
|
||||||
class AddressTypes(IntEnum):
|
class AddressTypes(IntEnum):
|
||||||
OFFER = auto()
|
OFFER = auto()
|
||||||
@@ -98,6 +101,8 @@ class BidStates(IntEnum):
|
|||||||
BID_STATE_UNKNOWN = 26
|
BID_STATE_UNKNOWN = 26
|
||||||
XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS = 27 # XmrBidLockTxSigsMessage
|
XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS = 27 # XmrBidLockTxSigsMessage
|
||||||
XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX = 28 # XmrBidLockSpendTxMessage
|
XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX = 28 # XmrBidLockSpendTxMessage
|
||||||
|
BID_REQUEST_SENT = 29
|
||||||
|
BID_REQUEST_ACCEPTED = 30
|
||||||
|
|
||||||
|
|
||||||
class TxStates(IntEnum):
|
class TxStates(IntEnum):
|
||||||
@@ -106,6 +111,8 @@ class TxStates(IntEnum):
|
|||||||
TX_CONFIRMED = auto()
|
TX_CONFIRMED = auto()
|
||||||
TX_REDEEMED = auto()
|
TX_REDEEMED = auto()
|
||||||
TX_REFUNDED = auto()
|
TX_REFUNDED = auto()
|
||||||
|
TX_IN_MEMPOOL = auto()
|
||||||
|
TX_IN_CHAIN = auto()
|
||||||
|
|
||||||
|
|
||||||
class TxTypes(IntEnum):
|
class TxTypes(IntEnum):
|
||||||
@@ -122,6 +129,10 @@ class TxTypes(IntEnum):
|
|||||||
XMR_SWAP_A_LOCK_REFUND_SPEND = auto()
|
XMR_SWAP_A_LOCK_REFUND_SPEND = auto()
|
||||||
XMR_SWAP_A_LOCK_REFUND_SWIPE = auto()
|
XMR_SWAP_A_LOCK_REFUND_SWIPE = auto()
|
||||||
XMR_SWAP_B_LOCK = auto()
|
XMR_SWAP_B_LOCK = auto()
|
||||||
|
XMR_SWAP_B_LOCK_SPEND = auto()
|
||||||
|
XMR_SWAP_B_LOCK_REFUND = auto()
|
||||||
|
|
||||||
|
ITX_PRE_FUNDED = auto()
|
||||||
|
|
||||||
|
|
||||||
class ActionTypes(IntEnum):
|
class ActionTypes(IntEnum):
|
||||||
@@ -136,6 +147,7 @@ class ActionTypes(IntEnum):
|
|||||||
RECOVER_XMR_SWAP_LOCK_TX_B = auto()
|
RECOVER_XMR_SWAP_LOCK_TX_B = auto()
|
||||||
SEND_XMR_SWAP_LOCK_SPEND_MSG = auto()
|
SEND_XMR_SWAP_LOCK_SPEND_MSG = auto()
|
||||||
REDEEM_ITX = auto()
|
REDEEM_ITX = auto()
|
||||||
|
ACCEPT_AS_REV_BID = auto()
|
||||||
|
|
||||||
|
|
||||||
class EventLogTypes(IntEnum):
|
class EventLogTypes(IntEnum):
|
||||||
@@ -162,6 +174,13 @@ class EventLogTypes(IntEnum):
|
|||||||
ERROR = auto()
|
ERROR = auto()
|
||||||
AUTOMATION_CONSTRAINT = auto()
|
AUTOMATION_CONSTRAINT = auto()
|
||||||
AUTOMATION_ACCEPTING_BID = auto()
|
AUTOMATION_ACCEPTING_BID = auto()
|
||||||
|
ITX_PUBLISHED = auto()
|
||||||
|
ITX_REDEEM_PUBLISHED = auto()
|
||||||
|
ITX_REFUND_PUBLISHED = auto()
|
||||||
|
PTX_PUBLISHED = auto()
|
||||||
|
PTX_REDEEM_PUBLISHED = auto()
|
||||||
|
PTX_REFUND_PUBLISHED = auto()
|
||||||
|
LOCK_TX_B_IN_MEMPOOL = auto()
|
||||||
|
|
||||||
|
|
||||||
class XmrSplitMsgTypes(IntEnum):
|
class XmrSplitMsgTypes(IntEnum):
|
||||||
@@ -178,6 +197,48 @@ class DebugTypes(IntEnum):
|
|||||||
MAKE_INVALID_PTX = auto()
|
MAKE_INVALID_PTX = auto()
|
||||||
DONT_SPEND_ITX = auto()
|
DONT_SPEND_ITX = auto()
|
||||||
SKIP_LOCK_TX_REFUND = auto()
|
SKIP_LOCK_TX_REFUND = auto()
|
||||||
|
SEND_LOCKED_XMR = auto()
|
||||||
|
B_LOCK_TX_MISSED_SEND = auto()
|
||||||
|
DUPLICATE_ACTIONS = auto()
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationTypes(IntEnum):
|
||||||
|
NONE = 0
|
||||||
|
OFFER_RECEIVED = auto()
|
||||||
|
BID_RECEIVED = auto()
|
||||||
|
BID_ACCEPTED = auto()
|
||||||
|
|
||||||
|
|
||||||
|
class AutomationOverrideOptions(IntEnum):
|
||||||
|
DEFAULT = 0
|
||||||
|
ALWAYS_ACCEPT = 1
|
||||||
|
NEVER_ACCEPT = auto()
|
||||||
|
|
||||||
|
|
||||||
|
def strAutomationOverrideOption(option):
|
||||||
|
if option == AutomationOverrideOptions.DEFAULT:
|
||||||
|
return 'Default'
|
||||||
|
if option == AutomationOverrideOptions.ALWAYS_ACCEPT:
|
||||||
|
return 'Always Accept'
|
||||||
|
if option == AutomationOverrideOptions.NEVER_ACCEPT:
|
||||||
|
return 'Never Accept'
|
||||||
|
return 'Unknown'
|
||||||
|
|
||||||
|
|
||||||
|
class VisibilityOverrideOptions(IntEnum):
|
||||||
|
DEFAULT = 0
|
||||||
|
HIDE = 1
|
||||||
|
BLOCK = auto()
|
||||||
|
|
||||||
|
|
||||||
|
def strVisibilityOverrideOption(option):
|
||||||
|
if option == VisibilityOverrideOptions.DEFAULT:
|
||||||
|
return 'Default'
|
||||||
|
if option == VisibilityOverrideOptions.HIDE:
|
||||||
|
return 'Hide'
|
||||||
|
if option == VisibilityOverrideOptions.BLOCK:
|
||||||
|
return 'Block'
|
||||||
|
return 'Unknown'
|
||||||
|
|
||||||
|
|
||||||
def strOfferState(state):
|
def strOfferState(state):
|
||||||
@@ -190,13 +251,6 @@ def strOfferState(state):
|
|||||||
return 'Unknown'
|
return 'Unknown'
|
||||||
|
|
||||||
|
|
||||||
class NotificationTypes(IntEnum):
|
|
||||||
NONE = 0
|
|
||||||
OFFER_RECEIVED = auto()
|
|
||||||
BID_RECEIVED = auto()
|
|
||||||
BID_ACCEPTED = auto()
|
|
||||||
|
|
||||||
|
|
||||||
def strBidState(state):
|
def strBidState(state):
|
||||||
if state == BidStates.BID_SENT:
|
if state == BidStates.BID_SENT:
|
||||||
return 'Sent'
|
return 'Sent'
|
||||||
@@ -252,6 +306,13 @@ def strBidState(state):
|
|||||||
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:
|
||||||
|
return 'Request sent'
|
||||||
|
if state == BidStates.BID_REQUEST_ACCEPTED:
|
||||||
|
return 'Request accepted'
|
||||||
|
if state == BidStates.BID_STATE_UNKNOWN:
|
||||||
|
return 'Unknown bid state'
|
||||||
|
|
||||||
return 'Unknown' + ' ' + str(state)
|
return 'Unknown' + ' ' + str(state)
|
||||||
|
|
||||||
|
|
||||||
@@ -266,6 +327,10 @@ def strTxState(state):
|
|||||||
return 'Redeemed'
|
return 'Redeemed'
|
||||||
if state == TxStates.TX_REFUNDED:
|
if state == TxStates.TX_REFUNDED:
|
||||||
return 'Refunded'
|
return 'Refunded'
|
||||||
|
if state == TxStates.TX_IN_MEMPOOL:
|
||||||
|
return 'In Mempool'
|
||||||
|
if state == TxStates.TX_IN_CHAIN:
|
||||||
|
return 'In Chain'
|
||||||
return 'Unknown'
|
return 'Unknown'
|
||||||
|
|
||||||
|
|
||||||
@@ -282,6 +347,8 @@ def strTxType(tx_type):
|
|||||||
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:
|
||||||
|
return 'Funded mock initiate tx'
|
||||||
return 'Unknown'
|
return 'Unknown'
|
||||||
|
|
||||||
|
|
||||||
@@ -309,8 +376,6 @@ def getLockName(lock_type):
|
|||||||
|
|
||||||
|
|
||||||
def describeEventEntry(event_type, event_msg):
|
def describeEventEntry(event_type, event_msg):
|
||||||
if event_type == EventLogTypes.FAILED_TX_B_LOCK_PUBLISH:
|
|
||||||
return 'Failed to publish lock tx B'
|
|
||||||
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:
|
||||||
@@ -327,6 +392,8 @@ def describeEventEntry(event_type, event_msg):
|
|||||||
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:
|
||||||
|
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:
|
||||||
@@ -357,11 +424,25 @@ def describeEventEntry(event_type, event_msg):
|
|||||||
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:
|
||||||
|
return 'Initiate tx published'
|
||||||
|
if event_type == EventLogTypes.ITX_REDEEM_PUBLISHED:
|
||||||
|
return 'Initiate tx redeem tx published'
|
||||||
|
if event_type == EventLogTypes.ITX_REFUND_PUBLISHED:
|
||||||
|
return 'Initiate tx refund tx published'
|
||||||
|
if event_type == EventLogTypes.PTX_PUBLISHED:
|
||||||
|
return 'Participate tx published'
|
||||||
|
if event_type == EventLogTypes.PTX_REDEEM_PUBLISHED:
|
||||||
|
return 'Participate tx redeem tx published'
|
||||||
|
if event_type == EventLogTypes.PTX_REFUND_PUBLISHED:
|
||||||
|
return 'Participate tx refund 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:
|
||||||
|
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:
|
||||||
@@ -369,14 +450,14 @@ def getVoutByAddress(txjs, p2sh):
|
|||||||
raise ValueError('Address output not found in txn')
|
raise ValueError('Address output not found in txn')
|
||||||
|
|
||||||
|
|
||||||
def getVoutByP2WSH(txjs, p2wsh_hex):
|
def getVoutByScriptPubKey(txjs, scriptPubKey_hex: str) -> int:
|
||||||
for o in txjs['vout']:
|
for o in txjs['vout']:
|
||||||
try:
|
try:
|
||||||
if p2wsh_hex == o['scriptPubKey']['hex']:
|
if scriptPubKey_hex == o['scriptPubKey']['hex']:
|
||||||
return o['n']
|
return o['n']
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
raise ValueError('P2WSH 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'):
|
||||||
@@ -406,27 +487,62 @@ def getLastBidState(packed_states):
|
|||||||
return BidStates.BID_STATE_UNKNOWN
|
return BidStates.BID_STATE_UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
|
def strSwapType(swap_type):
|
||||||
|
if swap_type == SwapTypes.SELLER_FIRST:
|
||||||
|
return 'seller_first'
|
||||||
|
if swap_type == SwapTypes.XMR_SWAP:
|
||||||
|
return 'xmr_swap'
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def strSwapDesc(swap_type):
|
||||||
|
if swap_type == SwapTypes.SELLER_FIRST:
|
||||||
|
return 'Secret Hash'
|
||||||
|
if swap_type == SwapTypes.XMR_SWAP:
|
||||||
|
return 'Adaptor Sig'
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
inactive_states = [BidStates.SWAP_COMPLETED, BidStates.BID_ERROR, BidStates.BID_REJECTED, BidStates.SWAP_TIMEDOUT, BidStates.BID_ABANDONED]
|
||||||
|
|
||||||
|
|
||||||
def isActiveBidState(state):
|
def isActiveBidState(state):
|
||||||
if state >= BidStates.BID_ACCEPTED and state < BidStates.SWAP_COMPLETED:
|
if state >= BidStates.BID_ACCEPTED and state < BidStates.SWAP_COMPLETED:
|
||||||
return True
|
return True
|
||||||
if state == BidStates.SWAP_DELAYING:
|
return state in (
|
||||||
return True
|
BidStates.SWAP_DELAYING,
|
||||||
if state == BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX:
|
BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX,
|
||||||
return True
|
BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED,
|
||||||
if state == BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED:
|
BidStates.XMR_SWAP_NOSCRIPT_COIN_LOCKED,
|
||||||
return True
|
BidStates.XMR_SWAP_LOCK_RELEASED,
|
||||||
if state == BidStates.XMR_SWAP_NOSCRIPT_COIN_LOCKED:
|
BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED,
|
||||||
return True
|
BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED,
|
||||||
if state == BidStates.XMR_SWAP_LOCK_RELEASED:
|
BidStates.XMR_SWAP_SCRIPT_TX_PREREFUND,
|
||||||
return True
|
BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS,
|
||||||
if state == BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED:
|
BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX,
|
||||||
return True
|
BidStates.XMR_SWAP_FAILED,
|
||||||
if state == BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED:
|
BidStates.BID_REQUEST_ACCEPTED,
|
||||||
return True
|
)
|
||||||
if state == BidStates.XMR_SWAP_SCRIPT_TX_PREREFUND:
|
|
||||||
return True
|
|
||||||
if state == BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS:
|
def isErrorBidState(state):
|
||||||
return True
|
return state in (
|
||||||
if state == BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX:
|
BidStates.BID_STALLED_FOR_TEST,
|
||||||
return True
|
BidStates.BID_ERROR,
|
||||||
return False
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def isFailingBidState(state):
|
||||||
|
return state in (
|
||||||
|
BidStates.BID_STALLED_FOR_TEST,
|
||||||
|
BidStates.BID_ERROR,
|
||||||
|
BidStates.XMR_SWAP_SCRIPT_TX_PREREFUND,
|
||||||
|
BidStates.XMR_SWAP_NOSCRIPT_TX_RECOVERED,
|
||||||
|
BidStates.XMR_SWAP_FAILED_REFUNDED,
|
||||||
|
BidStates.XMR_SWAP_FAILED_SWIPED,
|
||||||
|
BidStates.XMR_SWAP_FAILED,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def isFinalBidState(state):
|
||||||
|
return state in inactive_states
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2019-2022 tecnovert
|
# Copyright (c) 2019-2023 tecnovert
|
||||||
# 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.
|
||||||
|
|
||||||
@@ -26,6 +26,13 @@ class Coins(IntEnum):
|
|||||||
XMR = 6
|
XMR = 6
|
||||||
PART_BLIND = 7
|
PART_BLIND = 7
|
||||||
PART_ANON = 8
|
PART_ANON = 8
|
||||||
|
# ZANO = 9
|
||||||
|
# NDAU = 10
|
||||||
|
PIVX = 11
|
||||||
|
DASH = 12
|
||||||
|
FIRO = 13
|
||||||
|
NAV = 14
|
||||||
|
LTC_MWEB = 15
|
||||||
|
|
||||||
|
|
||||||
chainparams = {
|
chainparams = {
|
||||||
@@ -206,10 +213,165 @@ chainparams = {
|
|||||||
'min_amount': 100000,
|
'min_amount': 100000,
|
||||||
'max_amount': 10000 * XMR_COIN,
|
'max_amount': 10000 * XMR_COIN,
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
Coins.PIVX: {
|
||||||
|
'name': 'pivx',
|
||||||
|
'ticker': 'PIVX',
|
||||||
|
'message_magic': 'DarkNet Signed Message:\n',
|
||||||
|
'blocks_target': 60 * 1,
|
||||||
|
'decimal_places': 8,
|
||||||
|
'has_cltv': True,
|
||||||
|
'has_csv': False,
|
||||||
|
'has_segwit': False,
|
||||||
|
'use_ticker_as_name': True,
|
||||||
|
'mainnet': {
|
||||||
|
'rpcport': 51473,
|
||||||
|
'pubkey_address': 30,
|
||||||
|
'script_address': 13,
|
||||||
|
'key_prefix': 212,
|
||||||
|
'bip44': 119,
|
||||||
|
'min_amount': 1000,
|
||||||
|
'max_amount': 100000 * COIN,
|
||||||
|
},
|
||||||
|
'testnet': {
|
||||||
|
'rpcport': 51475,
|
||||||
|
'pubkey_address': 139,
|
||||||
|
'script_address': 19,
|
||||||
|
'key_prefix': 239,
|
||||||
|
'bip44': 1,
|
||||||
|
'min_amount': 1000,
|
||||||
|
'max_amount': 100000 * COIN,
|
||||||
|
'name': 'testnet4',
|
||||||
|
},
|
||||||
|
'regtest': {
|
||||||
|
'rpcport': 51477,
|
||||||
|
'pubkey_address': 139,
|
||||||
|
'script_address': 19,
|
||||||
|
'key_prefix': 239,
|
||||||
|
'bip44': 1,
|
||||||
|
'min_amount': 1000,
|
||||||
|
'max_amount': 100000 * COIN,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Coins.DASH: {
|
||||||
|
'name': 'dash',
|
||||||
|
'ticker': 'DASH',
|
||||||
|
'message_magic': 'DarkCoin Signed Message:\n',
|
||||||
|
'blocks_target': 60 * 2.5,
|
||||||
|
'decimal_places': 8,
|
||||||
|
'has_csv': True,
|
||||||
|
'has_segwit': False,
|
||||||
|
'mainnet': {
|
||||||
|
'rpcport': 9998,
|
||||||
|
'pubkey_address': 76,
|
||||||
|
'script_address': 16,
|
||||||
|
'key_prefix': 204,
|
||||||
|
'hrp': '',
|
||||||
|
'bip44': 5,
|
||||||
|
'min_amount': 1000,
|
||||||
|
'max_amount': 100000 * COIN,
|
||||||
|
},
|
||||||
|
'testnet': {
|
||||||
|
'rpcport': 19998,
|
||||||
|
'pubkey_address': 140,
|
||||||
|
'script_address': 19,
|
||||||
|
'key_prefix': 239,
|
||||||
|
'hrp': '',
|
||||||
|
'bip44': 1,
|
||||||
|
'min_amount': 1000,
|
||||||
|
'max_amount': 100000 * 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: {
|
||||||
|
'name': 'firo',
|
||||||
|
'ticker': 'FIRO',
|
||||||
|
'message_magic': 'Zcoin Signed Message:\n',
|
||||||
|
'blocks_target': 60 * 10,
|
||||||
|
'decimal_places': 8,
|
||||||
|
'has_cltv': False,
|
||||||
|
'has_csv': False,
|
||||||
|
'has_segwit': False,
|
||||||
|
'mainnet': {
|
||||||
|
'rpcport': 8888,
|
||||||
|
'pubkey_address': 82,
|
||||||
|
'script_address': 7,
|
||||||
|
'key_prefix': 210,
|
||||||
|
'hrp': '',
|
||||||
|
'bip44': 136,
|
||||||
|
'min_amount': 1000,
|
||||||
|
'max_amount': 100000 * COIN,
|
||||||
|
},
|
||||||
|
'testnet': {
|
||||||
|
'rpcport': 18888,
|
||||||
|
'pubkey_address': 65,
|
||||||
|
'script_address': 178,
|
||||||
|
'key_prefix': 185,
|
||||||
|
'hrp': '',
|
||||||
|
'bip44': 1,
|
||||||
|
'min_amount': 1000,
|
||||||
|
'max_amount': 100000 * 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: {
|
||||||
|
'name': 'navcoin',
|
||||||
|
'ticker': 'NAV',
|
||||||
|
'message_magic': 'Navcoin Signed Message:\n',
|
||||||
|
'blocks_target': 30,
|
||||||
|
'decimal_places': 8,
|
||||||
|
'has_csv': True,
|
||||||
|
'has_segwit': True,
|
||||||
|
'mainnet': {
|
||||||
|
'rpcport': 44444,
|
||||||
|
'pubkey_address': 53,
|
||||||
|
'script_address': 85,
|
||||||
|
'key_prefix': 150,
|
||||||
|
'hrp': '',
|
||||||
|
'bip44': 130,
|
||||||
|
'min_amount': 1000,
|
||||||
|
'max_amount': 100000 * 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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ticker_map = {}
|
ticker_map = {}
|
||||||
|
|
||||||
|
|
||||||
@@ -234,17 +396,20 @@ class CoinInterface:
|
|||||||
self._unknown_wallet_seed = True
|
self._unknown_wallet_seed = True
|
||||||
self._restore_height = None
|
self._restore_height = None
|
||||||
|
|
||||||
def make_int(self, amount_in, r=0):
|
def make_int(self, amount_in: int, r: int = 0) -> int:
|
||||||
return make_int(amount_in, self.exp(), r=r)
|
return make_int(amount_in, self.exp(), r=r)
|
||||||
|
|
||||||
def format_amount(self, amount_in, conv_int=False, r=0):
|
def format_amount(self, amount_in, conv_int=False, r=0):
|
||||||
amount_int = make_int(amount_in, self.exp(), r=r) if conv_int else amount_in
|
amount_int = make_int(amount_in, self.exp(), r=r) if conv_int else amount_in
|
||||||
return format_amount(amount_int, self.exp())
|
return format_amount(amount_int, self.exp())
|
||||||
|
|
||||||
def coin_name(self):
|
def coin_name(self) -> str:
|
||||||
return chainparams[self.coin_type()]['name'].capitalize()
|
coin_chainparams = chainparams[self.coin_type()]
|
||||||
|
if coin_chainparams.get('use_ticker_as_name', False):
|
||||||
|
return coin_chainparams['ticker']
|
||||||
|
return coin_chainparams['name'].capitalize()
|
||||||
|
|
||||||
def ticker(self):
|
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
|
||||||
@@ -252,23 +417,29 @@ class CoinInterface:
|
|||||||
ticker = 'rt' + ticker
|
ticker = 'rt' + ticker
|
||||||
return ticker
|
return ticker
|
||||||
|
|
||||||
def ticker_mainnet(self):
|
def getExchangeTicker(self, exchange_name: str) -> str:
|
||||||
|
return chainparams[self.coin_type()]['ticker']
|
||||||
|
|
||||||
|
def getExchangeName(self, exchange_name: str) -> str:
|
||||||
|
return chainparams[self.coin_type()]['name']
|
||||||
|
|
||||||
|
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):
|
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):
|
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):
|
def setWalletSeedWarning(self, value: bool) -> None:
|
||||||
self._unknown_wallet_seed = value
|
self._unknown_wallet_seed = value
|
||||||
|
|
||||||
def setWalletRestoreHeight(self, value):
|
def setWalletRestoreHeight(self, value: int) -> None:
|
||||||
self._restore_height = value
|
self._restore_height = value
|
||||||
|
|
||||||
def knownWalletSeed(self):
|
def knownWalletSeed(self) -> bool:
|
||||||
return not self._unknown_wallet_seed
|
return not self._unknown_wallet_seed
|
||||||
|
|
||||||
def chainparams(self):
|
def chainparams(self):
|
||||||
@@ -277,16 +448,25 @@ class CoinInterface:
|
|||||||
def chainparams_network(self):
|
def chainparams_network(self):
|
||||||
return chainparams[self.coin_type()][self._network]
|
return chainparams[self.coin_type()][self._network]
|
||||||
|
|
||||||
def is_transient_error(self, ex):
|
def has_segwit(self) -> bool:
|
||||||
|
return chainparams[self.coin_type()].get('has_segwit', True)
|
||||||
|
|
||||||
|
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 = 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:
|
||||||
|
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:
|
||||||
|
return True
|
||||||
|
if 'request-sent' in str_error:
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2019-2022 tecnovert
|
# Copyright (c) 2019-2023 tecnovert
|
||||||
# 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.
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ CONFIG_FILENAME = 'basicswap.json'
|
|||||||
BASICSWAP_DATADIR = os.getenv('BASICSWAP_DATADIR', '~/.basicswap')
|
BASICSWAP_DATADIR = os.getenv('BASICSWAP_DATADIR', '~/.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', '~/tmp/bin'))
|
DEFAULT_TEST_BINDIR = os.path.expanduser(os.getenv('DEFAULT_TEST_BINDIR', '~/.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(os.getenv('PARTICL_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'particl')))
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2019-2022 tecnovert
|
# Copyright (c) 2019-2023 tecnovert
|
||||||
# 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,8 +12,8 @@ from enum import IntEnum, auto
|
|||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
|
|
||||||
CURRENT_DB_VERSION = 15
|
CURRENT_DB_VERSION = 22
|
||||||
CURRENT_DB_DATA_VERSION = 2
|
CURRENT_DB_DATA_VERSION = 4
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
@@ -67,6 +67,7 @@ class Offer(Base):
|
|||||||
|
|
||||||
proof_address = sa.Column(sa.String)
|
proof_address = sa.Column(sa.String)
|
||||||
proof_signature = sa.Column(sa.LargeBinary)
|
proof_signature = sa.Column(sa.LargeBinary)
|
||||||
|
proof_utxos = sa.Column(sa.LargeBinary)
|
||||||
pkhash_seller = sa.Column(sa.LargeBinary)
|
pkhash_seller = sa.Column(sa.LargeBinary)
|
||||||
secret_hash = sa.Column(sa.LargeBinary)
|
secret_hash = sa.Column(sa.LargeBinary)
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ class Offer(Base):
|
|||||||
addr_to = sa.Column(sa.String)
|
addr_to = sa.Column(sa.String)
|
||||||
created_at = sa.Column(sa.BigInteger)
|
created_at = sa.Column(sa.BigInteger)
|
||||||
expire_at = sa.Column(sa.BigInteger)
|
expire_at = sa.Column(sa.BigInteger)
|
||||||
was_sent = sa.Column(sa.Boolean)
|
was_sent = sa.Column(sa.Boolean) # Sent by node
|
||||||
|
|
||||||
from_feerate = sa.Column(sa.BigInteger)
|
from_feerate = sa.Column(sa.BigInteger)
|
||||||
to_feerate = sa.Column(sa.BigInteger)
|
to_feerate = sa.Column(sa.BigInteger)
|
||||||
@@ -86,6 +87,7 @@ class Offer(Base):
|
|||||||
auto_accept_bids = sa.Column(sa.Boolean)
|
auto_accept_bids = sa.Column(sa.Boolean)
|
||||||
withdraw_to_addr = sa.Column(sa.String) # Address to spend lock tx to - address from wallet if empty TODO
|
withdraw_to_addr = sa.Column(sa.String) # Address to spend lock tx to - address from wallet if empty TODO
|
||||||
security_token = sa.Column(sa.LargeBinary)
|
security_token = sa.Column(sa.LargeBinary)
|
||||||
|
bid_reversed = sa.Column(sa.Boolean)
|
||||||
|
|
||||||
state = sa.Column(sa.Integer)
|
state = sa.Column(sa.Integer)
|
||||||
states = sa.Column(sa.LargeBinary) # Packed states and times
|
states = sa.Column(sa.LargeBinary) # Packed states and times
|
||||||
@@ -107,13 +109,14 @@ class Bid(Base):
|
|||||||
active_ind = sa.Column(sa.Integer)
|
active_ind = sa.Column(sa.Integer)
|
||||||
|
|
||||||
protocol_version = sa.Column(sa.Integer)
|
protocol_version = sa.Column(sa.Integer)
|
||||||
was_sent = sa.Column(sa.Boolean)
|
was_sent = sa.Column(sa.Boolean) # Sent by node
|
||||||
was_received = sa.Column(sa.Boolean)
|
was_received = sa.Column(sa.Boolean)
|
||||||
contract_count = sa.Column(sa.Integer)
|
contract_count = sa.Column(sa.Integer)
|
||||||
created_at = sa.Column(sa.BigInteger)
|
created_at = sa.Column(sa.BigInteger)
|
||||||
expire_at = sa.Column(sa.BigInteger)
|
expire_at = sa.Column(sa.BigInteger)
|
||||||
bid_addr = sa.Column(sa.String)
|
bid_addr = sa.Column(sa.String)
|
||||||
proof_address = sa.Column(sa.String)
|
proof_address = sa.Column(sa.String)
|
||||||
|
proof_utxos = sa.Column(sa.LargeBinary)
|
||||||
withdraw_to_addr = sa.Column(sa.String) # Address to spend lock tx to - address from wallet if empty TODO
|
withdraw_to_addr = sa.Column(sa.String) # Address to spend lock tx to - address from wallet if empty TODO
|
||||||
|
|
||||||
recovered_secret = sa.Column(sa.LargeBinary)
|
recovered_secret = sa.Column(sa.LargeBinary)
|
||||||
@@ -123,7 +126,6 @@ class Bid(Base):
|
|||||||
amount = sa.Column(sa.BigInteger)
|
amount = sa.Column(sa.BigInteger)
|
||||||
rate = sa.Column(sa.BigInteger)
|
rate = sa.Column(sa.BigInteger)
|
||||||
|
|
||||||
accept_msg_id = sa.Column(sa.LargeBinary)
|
|
||||||
pkhash_seller = sa.Column(sa.LargeBinary)
|
pkhash_seller = sa.Column(sa.LargeBinary)
|
||||||
|
|
||||||
initiate_txn_redeem = sa.Column(sa.LargeBinary)
|
initiate_txn_redeem = sa.Column(sa.LargeBinary)
|
||||||
@@ -217,10 +219,25 @@ class SwapTx(Base):
|
|||||||
states = sa.Column(sa.LargeBinary) # Packed states and times
|
states = sa.Column(sa.LargeBinary) # Packed states and times
|
||||||
|
|
||||||
def setState(self, new_state):
|
def setState(self, new_state):
|
||||||
|
if self.state == new_state:
|
||||||
|
return
|
||||||
self.state = new_state
|
self.state = new_state
|
||||||
self.states = (self.states if self.states is not None else bytes()) + struct.pack('<iq', new_state, int(time.time()))
|
self.states = (self.states if self.states is not None else bytes()) + struct.pack('<iq', new_state, int(time.time()))
|
||||||
|
|
||||||
|
|
||||||
|
class PrefundedTx(Base):
|
||||||
|
__tablename__ = 'prefunded_transactions'
|
||||||
|
|
||||||
|
record_id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
|
||||||
|
active_ind = sa.Column(sa.Integer)
|
||||||
|
created_at = sa.Column(sa.BigInteger)
|
||||||
|
linked_type = sa.Column(sa.Integer)
|
||||||
|
linked_id = sa.Column(sa.LargeBinary)
|
||||||
|
tx_type = sa.Column(sa.Integer) # TxTypes
|
||||||
|
tx_data = sa.Column(sa.LargeBinary)
|
||||||
|
used_by = sa.Column(sa.LargeBinary)
|
||||||
|
|
||||||
|
|
||||||
class PooledAddress(Base):
|
class PooledAddress(Base):
|
||||||
__tablename__ = 'addresspool'
|
__tablename__ = 'addresspool'
|
||||||
|
|
||||||
@@ -277,12 +294,13 @@ class EventLog(Base):
|
|||||||
|
|
||||||
class XmrOffer(Base):
|
class XmrOffer(Base):
|
||||||
__tablename__ = 'xmr_offers'
|
__tablename__ = 'xmr_offers'
|
||||||
|
# TODO: Merge to Offer
|
||||||
|
|
||||||
swap_id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
|
swap_id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
|
||||||
offer_id = sa.Column(sa.LargeBinary, sa.ForeignKey('offers.offer_id'))
|
offer_id = sa.Column(sa.LargeBinary, sa.ForeignKey('offers.offer_id'))
|
||||||
|
|
||||||
a_fee_rate = sa.Column(sa.BigInteger)
|
a_fee_rate = sa.Column(sa.BigInteger) # Chain a fee rate
|
||||||
b_fee_rate = sa.Column(sa.BigInteger)
|
b_fee_rate = sa.Column(sa.BigInteger) # Chain b fee rate
|
||||||
|
|
||||||
lock_time_1 = sa.Column(sa.Integer) # Delay before the chain a lock refund tx can be mined
|
lock_time_1 = sa.Column(sa.Integer) # Delay before the chain a lock refund tx can be mined
|
||||||
lock_time_2 = sa.Column(sa.Integer) # Delay before the follower can spend from the chain a lock refund tx
|
lock_time_2 = sa.Column(sa.Integer) # Delay before the follower can spend from the chain a lock refund tx
|
||||||
@@ -293,16 +311,6 @@ class XmrSwap(Base):
|
|||||||
|
|
||||||
swap_id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
|
swap_id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
|
||||||
bid_id = sa.Column(sa.LargeBinary, sa.ForeignKey('bids.bid_id'))
|
bid_id = sa.Column(sa.LargeBinary, sa.ForeignKey('bids.bid_id'))
|
||||||
bid_msg_id2 = sa.Column(sa.LargeBinary)
|
|
||||||
bid_msg_id3 = sa.Column(sa.LargeBinary)
|
|
||||||
|
|
||||||
bid_accept_msg_id = sa.Column(sa.LargeBinary)
|
|
||||||
bid_accept_msg_id2 = sa.Column(sa.LargeBinary)
|
|
||||||
bid_accept_msg_id3 = sa.Column(sa.LargeBinary)
|
|
||||||
|
|
||||||
coin_a_lock_tx_sigs_l_msg_id = sa.Column(sa.LargeBinary) # MSG3L F -> L
|
|
||||||
coin_a_lock_spend_tx_msg_id = sa.Column(sa.LargeBinary) # MSG4F L -> F
|
|
||||||
coin_a_lock_release_msg_id = sa.Column(sa.LargeBinary) # MSG5F L -> F
|
|
||||||
|
|
||||||
contract_count = sa.Column(sa.Integer)
|
contract_count = sa.Column(sa.Integer)
|
||||||
|
|
||||||
@@ -363,6 +371,8 @@ class XmrSplitData(Base):
|
|||||||
__tablename__ = 'xmr_split_data'
|
__tablename__ = 'xmr_split_data'
|
||||||
|
|
||||||
record_id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
|
record_id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
|
||||||
|
addr_from = sa.Column(sa.String)
|
||||||
|
addr_to = sa.Column(sa.String)
|
||||||
bid_id = sa.Column(sa.LargeBinary)
|
bid_id = sa.Column(sa.LargeBinary)
|
||||||
msg_type = sa.Column(sa.Integer)
|
msg_type = sa.Column(sa.Integer)
|
||||||
msg_sequence = sa.Column(sa.Integer)
|
msg_sequence = sa.Column(sa.Integer)
|
||||||
@@ -408,6 +418,9 @@ class KnownIdentity(Base):
|
|||||||
num_recv_bids_rejected = sa.Column(sa.Integer)
|
num_recv_bids_rejected = sa.Column(sa.Integer)
|
||||||
num_sent_bids_failed = sa.Column(sa.Integer)
|
num_sent_bids_failed = sa.Column(sa.Integer)
|
||||||
num_recv_bids_failed = sa.Column(sa.Integer)
|
num_recv_bids_failed = sa.Column(sa.Integer)
|
||||||
|
automation_override = sa.Column(sa.Integer) # AutomationOverrideOptions
|
||||||
|
visibility_override = sa.Column(sa.Integer) # VisibilityOverrideOptions
|
||||||
|
data = sa.Column(sa.LargeBinary)
|
||||||
note = sa.Column(sa.String)
|
note = sa.Column(sa.String)
|
||||||
updated_at = sa.Column(sa.BigInteger)
|
updated_at = sa.Column(sa.BigInteger)
|
||||||
created_at = sa.Column(sa.BigInteger)
|
created_at = sa.Column(sa.BigInteger)
|
||||||
@@ -469,6 +482,35 @@ class BidState(Base):
|
|||||||
state_id = sa.Column(sa.Integer)
|
state_id = sa.Column(sa.Integer)
|
||||||
label = sa.Column(sa.String)
|
label = sa.Column(sa.String)
|
||||||
in_progress = sa.Column(sa.Integer)
|
in_progress = sa.Column(sa.Integer)
|
||||||
|
in_error = sa.Column(sa.Integer)
|
||||||
|
swap_failed = sa.Column(sa.Integer)
|
||||||
|
swap_ended = sa.Column(sa.Integer)
|
||||||
|
|
||||||
note = sa.Column(sa.String)
|
note = sa.Column(sa.String)
|
||||||
created_at = sa.Column(sa.BigInteger)
|
created_at = sa.Column(sa.BigInteger)
|
||||||
|
|
||||||
|
|
||||||
|
class Notification(Base):
|
||||||
|
__tablename__ = 'notifications'
|
||||||
|
|
||||||
|
record_id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
|
||||||
|
active_ind = sa.Column(sa.Integer)
|
||||||
|
created_at = sa.Column(sa.BigInteger)
|
||||||
|
event_type = sa.Column(sa.Integer)
|
||||||
|
event_data = sa.Column(sa.LargeBinary)
|
||||||
|
|
||||||
|
|
||||||
|
class MessageLink(Base):
|
||||||
|
__tablename__ = 'message_links'
|
||||||
|
|
||||||
|
record_id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
|
||||||
|
active_ind = sa.Column(sa.Integer)
|
||||||
|
created_at = sa.Column(sa.BigInteger)
|
||||||
|
|
||||||
|
linked_type = sa.Column(sa.Integer)
|
||||||
|
linked_id = sa.Column(sa.LargeBinary)
|
||||||
|
# linked_row_id = sa.Column(sa.Integer) # TODO: Find a way to use table rowids
|
||||||
|
|
||||||
|
msg_type = sa.Column(sa.Integer)
|
||||||
|
msg_sequence = sa.Column(sa.Integer)
|
||||||
|
msg_id = sa.Column(sa.LargeBinary)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2022 tecnovert
|
# Copyright (c) 2022-2023 tecnovert
|
||||||
# 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,7 +18,11 @@ from .db import (
|
|||||||
from .basicswap_util import (
|
from .basicswap_util import (
|
||||||
BidStates,
|
BidStates,
|
||||||
strBidState,
|
strBidState,
|
||||||
isActiveBidState)
|
isActiveBidState,
|
||||||
|
isErrorBidState,
|
||||||
|
isFailingBidState,
|
||||||
|
isFinalBidState,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def upgradeDatabaseData(self, data_version):
|
def upgradeDatabaseData(self, data_version):
|
||||||
@@ -56,10 +60,13 @@ def upgradeDatabaseData(self, data_version):
|
|||||||
active_ind=1,
|
active_ind=1,
|
||||||
state_id=int(state),
|
state_id=int(state),
|
||||||
in_progress=isActiveBidState(state),
|
in_progress=isActiveBidState(state),
|
||||||
|
in_error=isErrorBidState(state),
|
||||||
|
swap_failed=isFailingBidState(state),
|
||||||
|
swap_ended=isFinalBidState(state),
|
||||||
label=strBidState(state),
|
label=strBidState(state),
|
||||||
created_at=now))
|
created_at=now))
|
||||||
|
|
||||||
if 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 (BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS, BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX):
|
||||||
session.add(BidState(
|
session.add(BidState(
|
||||||
active_ind=1,
|
active_ind=1,
|
||||||
@@ -67,6 +74,23 @@ def upgradeDatabaseData(self, data_version):
|
|||||||
in_progress=isActiveBidState(state),
|
in_progress=isActiveBidState(state),
|
||||||
label=strBidState(state),
|
label=strBidState(state),
|
||||||
created_at=now))
|
created_at=now))
|
||||||
|
if data_version > 0 and data_version < 3:
|
||||||
|
for state in BidStates:
|
||||||
|
in_error = isErrorBidState(state)
|
||||||
|
swap_failed = isFailingBidState(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)})
|
||||||
|
if data_version > 0 and data_version < 4:
|
||||||
|
for state in (BidStates.BID_REQUEST_SENT, BidStates.BID_REQUEST_ACCEPTED):
|
||||||
|
session.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),
|
||||||
|
label=strBidState(state),
|
||||||
|
created_at=now))
|
||||||
|
|
||||||
self.db_data_version = CURRENT_DB_DATA_VERSION
|
self.db_data_version = CURRENT_DB_DATA_VERSION
|
||||||
self.setIntKVInSession('db_data_version', self.db_data_version, session)
|
self.setIntKVInSession('db_data_version', self.db_data_version, session)
|
||||||
@@ -215,6 +239,64 @@ def upgradeDatabase(self, db_version):
|
|||||||
db_version += 1
|
db_version += 1
|
||||||
session.execute('ALTER TABLE xmr_swaps ADD COLUMN coin_a_lock_release_msg_id BLOB')
|
session.execute('ALTER TABLE xmr_swaps ADD COLUMN coin_a_lock_release_msg_id BLOB')
|
||||||
session.execute('ALTER TABLE xmr_swaps RENAME COLUMN coin_a_lock_refund_spend_tx_msg_id TO coin_a_lock_spend_tx_msg_id')
|
session.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:
|
||||||
|
db_version += 1
|
||||||
|
session.execute('''
|
||||||
|
CREATE TABLE notifications (
|
||||||
|
record_id INTEGER NOT NULL,
|
||||||
|
active_ind INTEGER,
|
||||||
|
event_type INTEGER,
|
||||||
|
event_data BLOB,
|
||||||
|
created_at BIGINT,
|
||||||
|
PRIMARY KEY (record_id))''')
|
||||||
|
elif current_version == 16:
|
||||||
|
db_version += 1
|
||||||
|
session.execute('''
|
||||||
|
CREATE TABLE prefunded_transactions (
|
||||||
|
record_id INTEGER NOT NULL,
|
||||||
|
active_ind INTEGER,
|
||||||
|
created_at BIGINT,
|
||||||
|
linked_type INTEGER,
|
||||||
|
linked_id BLOB,
|
||||||
|
tx_type INTEGER,
|
||||||
|
tx_data BLOB,
|
||||||
|
used_by BLOB,
|
||||||
|
PRIMARY KEY (record_id))''')
|
||||||
|
elif current_version == 17:
|
||||||
|
db_version += 1
|
||||||
|
session.execute('ALTER TABLE knownidentities ADD COLUMN automation_override INTEGER')
|
||||||
|
session.execute('ALTER TABLE knownidentities ADD COLUMN visibility_override INTEGER')
|
||||||
|
session.execute('ALTER TABLE knownidentities ADD COLUMN data BLOB')
|
||||||
|
session.execute('UPDATE knownidentities SET active_ind = 1')
|
||||||
|
elif current_version == 18:
|
||||||
|
db_version += 1
|
||||||
|
session.execute('ALTER TABLE xmr_split_data ADD COLUMN addr_from STRING')
|
||||||
|
session.execute('ALTER TABLE xmr_split_data ADD COLUMN addr_to STRING')
|
||||||
|
elif current_version == 19:
|
||||||
|
db_version += 1
|
||||||
|
session.execute('ALTER TABLE bidstates ADD COLUMN in_error INTEGER')
|
||||||
|
session.execute('ALTER TABLE bidstates ADD COLUMN swap_failed INTEGER')
|
||||||
|
session.execute('ALTER TABLE bidstates ADD COLUMN swap_ended INTEGER')
|
||||||
|
elif current_version == 20:
|
||||||
|
db_version += 1
|
||||||
|
session.execute('''
|
||||||
|
CREATE TABLE message_links (
|
||||||
|
record_id INTEGER NOT NULL,
|
||||||
|
active_ind INTEGER,
|
||||||
|
created_at BIGINT,
|
||||||
|
|
||||||
|
linked_type INTEGER,
|
||||||
|
linked_id BLOB,
|
||||||
|
|
||||||
|
msg_type INTEGER,
|
||||||
|
msg_sequence INTEGER,
|
||||||
|
msg_id BLOB,
|
||||||
|
PRIMARY KEY (record_id))''')
|
||||||
|
session.execute('ALTER TABLE offers ADD COLUMN bid_reversed INTEGER')
|
||||||
|
elif current_version == 21:
|
||||||
|
db_version += 1
|
||||||
|
session.execute('ALTER TABLE offers ADD COLUMN proof_utxos BLOB')
|
||||||
|
session.execute('ALTER TABLE bids ADD COLUMN proof_utxos BLOB')
|
||||||
|
|
||||||
if current_version != db_version:
|
if current_version != db_version:
|
||||||
self.db_version = db_version
|
self.db_version = db_version
|
||||||
|
|||||||
56
basicswap/db_util.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2023 The BSX Developers
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
|
from .db import (
|
||||||
|
Concepts,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_expired_data(self, time_offset: int = 0):
|
||||||
|
now: int = self.getTime()
|
||||||
|
try:
|
||||||
|
session = self.openSession()
|
||||||
|
|
||||||
|
active_bids_insert = self.activeBidsQueryStr(now, '', 'b2')
|
||||||
|
query_str = f'''
|
||||||
|
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})
|
||||||
|
'''
|
||||||
|
num_offers = 0
|
||||||
|
num_bids = 0
|
||||||
|
offer_rows = session.execute(query_str, {'expired_at': now - time_offset})
|
||||||
|
for offer_row in offer_rows:
|
||||||
|
num_offers += 1
|
||||||
|
bid_rows = session.execute('SELECT bids.bid_id FROM bids WHERE bids.offer_id = :offer_id', {'offer_id': offer_row[0]})
|
||||||
|
for bid_row in bid_rows:
|
||||||
|
num_bids += 1
|
||||||
|
session.execute('DELETE FROM transactions WHERE transactions.bid_id = :bid_id', {'bid_id': bid_row[0]})
|
||||||
|
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]})
|
||||||
|
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]})
|
||||||
|
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]})
|
||||||
|
session.execute('DELETE FROM xmr_swaps WHERE xmr_swaps.bid_id = :bid_id', {'bid_id': bid_row[0]})
|
||||||
|
session.execute('DELETE FROM actions WHERE actions.linked_id = :bid_id', {'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]})
|
||||||
|
session.execute('DELETE FROM bids WHERE bids.bid_id = :bid_id', {'bid_id': bid_row[0]})
|
||||||
|
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]})
|
||||||
|
|
||||||
|
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]})
|
||||||
|
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]})
|
||||||
|
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]})
|
||||||
|
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]})
|
||||||
|
session.execute('DELETE FROM sentoffers WHERE sentoffers.offer_id = :offer_id', {'offer_id': offer_row[0]})
|
||||||
|
session.execute('DELETE FROM actions WHERE actions.linked_id = :offer_id', {'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]})
|
||||||
|
|
||||||
|
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 ''))
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self.closeSession(session)
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2019-2022 tecnovert
|
# Copyright (c) 2019-2023 tecnovert
|
||||||
# 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 urllib.request
|
|
||||||
|
|
||||||
|
|
||||||
class Explorer():
|
class Explorer():
|
||||||
@@ -17,12 +16,7 @@ class Explorer():
|
|||||||
|
|
||||||
def readURL(self, url):
|
def readURL(self, url):
|
||||||
self.log.debug('Explorer url: {}'.format(url))
|
self.log.debug('Explorer url: {}'.format(url))
|
||||||
try:
|
return self.swapclient.readURL(url)
|
||||||
self.swapclient.setConnectionParameters()
|
|
||||||
req = urllib.request.Request(url)
|
|
||||||
return urllib.request.urlopen(req).read()
|
|
||||||
finally:
|
|
||||||
self.swapclient.popConnectionParameters()
|
|
||||||
|
|
||||||
|
|
||||||
class ExplorerInsight(Explorer):
|
class ExplorerInsight(Explorer):
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2019-2022 tecnovert
|
# Copyright (c) 2019-2024 tecnovert
|
||||||
# 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
|
||||||
import json
|
import json
|
||||||
|
import shlex
|
||||||
import traceback
|
import traceback
|
||||||
import threading
|
import threading
|
||||||
import http.client
|
import http.client
|
||||||
import urllib.parse
|
from urllib import parse
|
||||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
from jinja2 import Environment, PackageLoader
|
from jinja2 import Environment, PackageLoader
|
||||||
|
|
||||||
from . import __version__
|
from . import __version__
|
||||||
from .util import (
|
from .util import (
|
||||||
dumpj,
|
dumpj,
|
||||||
ensure,
|
|
||||||
toBool,
|
toBool,
|
||||||
|
LockedCoinError,
|
||||||
format_timestamp,
|
format_timestamp,
|
||||||
)
|
)
|
||||||
from .chainparams import (
|
from .chainparams import (
|
||||||
@@ -25,65 +26,40 @@ from .chainparams import (
|
|||||||
chainparams,
|
chainparams,
|
||||||
)
|
)
|
||||||
from .basicswap_util import (
|
from .basicswap_util import (
|
||||||
BidStates,
|
|
||||||
SwapTypes,
|
|
||||||
DebugTypes,
|
|
||||||
strBidState,
|
|
||||||
strTxState,
|
strTxState,
|
||||||
strAddressType,
|
strBidState,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .js_server import (
|
from .js_server import (
|
||||||
js_coins,
|
|
||||||
js_error,
|
js_error,
|
||||||
js_wallets,
|
js_url_to_function,
|
||||||
js_offers,
|
|
||||||
js_sentoffers,
|
|
||||||
js_bids,
|
|
||||||
js_sentbids,
|
|
||||||
js_network,
|
|
||||||
js_revokeoffer,
|
|
||||||
js_smsgaddresses,
|
|
||||||
js_rates_list,
|
|
||||||
js_rates,
|
|
||||||
js_rate,
|
|
||||||
js_index,
|
|
||||||
)
|
)
|
||||||
from .ui.util import (
|
from .ui.util import (
|
||||||
PAGE_LIMIT,
|
|
||||||
describeBid,
|
|
||||||
getCoinName,
|
getCoinName,
|
||||||
listBidStates,
|
|
||||||
get_data_entry,
|
get_data_entry,
|
||||||
have_data_entry,
|
|
||||||
listOldBidStates,
|
|
||||||
get_data_entry_or,
|
get_data_entry_or,
|
||||||
listAvailableCoins,
|
listAvailableCoins,
|
||||||
set_pagination_filters,
|
|
||||||
)
|
)
|
||||||
from .ui.page_tor import page_tor
|
|
||||||
from .ui.page_offers import page_offers, page_offer, page_newoffer
|
|
||||||
from .ui.page_wallet import page_wallets, page_wallet
|
|
||||||
from .ui.page_automation import (
|
from .ui.page_automation import (
|
||||||
page_automation_strategies,
|
page_automation_strategies,
|
||||||
page_automation_strategy,
|
page_automation_strategy,
|
||||||
page_automation_strategy_new,
|
page_automation_strategy_new,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .ui.page_bids import page_bids, page_bid
|
||||||
|
from .ui.page_offers import page_offers, page_offer, page_newoffer
|
||||||
|
from .ui.page_tor import page_tor, get_tor_established_state
|
||||||
|
from .ui.page_wallet import page_wallets, page_wallet
|
||||||
|
from .ui.page_settings import page_settings
|
||||||
|
from .ui.page_encryption import page_changepassword, page_unlock, page_lock
|
||||||
|
from .ui.page_identity import page_identity
|
||||||
|
from .ui.page_smsgaddresses import page_smsgaddresses
|
||||||
|
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 validateTextInput(text, name, messages, max_length=None):
|
|
||||||
if max_length is not None and len(text) > max_length:
|
|
||||||
messages.append(f'Error: {name} is too long')
|
|
||||||
return False
|
|
||||||
if len(text) > 0 and all(c.isalnum() or c.isspace() for c in text) is False:
|
|
||||||
messages.append(f'Error: {name} must consist of only letters and digits')
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def extractDomain(url):
|
def extractDomain(url):
|
||||||
return url.split('://', 1)[1].split('/', 1)[0]
|
return url.split('://', 1)[1].split('/', 1)[0]
|
||||||
|
|
||||||
@@ -94,7 +70,7 @@ def listAvailableExplorers(swap_client):
|
|||||||
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), swap_client.coin_clients[c]['name'].capitalize() + ' - ' + extractDomain(e.base_url)))
|
explorers.append(('{}_{}'.format(int(c), i), getCoinName(c) + ' - ' + extractDomain(e.base_url)))
|
||||||
return explorers
|
return explorers
|
||||||
|
|
||||||
|
|
||||||
@@ -107,60 +83,146 @@ def listExplorerActions(swap_client):
|
|||||||
return actions
|
return actions
|
||||||
|
|
||||||
|
|
||||||
def html_content_start(title, h2=None, refresh=None):
|
def parse_cmd(cmd: str, type_map: str):
|
||||||
content = '<!DOCTYPE html><html lang="en">\n<head>' \
|
params = shlex.split(cmd)
|
||||||
+ '<meta charset="UTF-8">' \
|
if len(params) < 1:
|
||||||
+ ('' if not refresh else '<meta http-equiv="refresh" content="{}">'.format(refresh)) \
|
return '', []
|
||||||
+ '<title>' + title + '</title></head>\n' \
|
method = params[0]
|
||||||
+ '<body>'
|
typed_params = []
|
||||||
if h2 is not None:
|
params = params[1:]
|
||||||
content += '<h2>' + h2 + '</h2>'
|
|
||||||
return content
|
for i, param in enumerate(params):
|
||||||
|
if i >= len(type_map):
|
||||||
|
type_ind = 's'
|
||||||
|
else:
|
||||||
|
type_ind = type_map[i]
|
||||||
|
if type_ind == 'i':
|
||||||
|
typed_params.append(int(param))
|
||||||
|
elif type_ind == 'b':
|
||||||
|
typed_params.append(toBool(param))
|
||||||
|
elif type_ind == 'j':
|
||||||
|
typed_params.append(json.loads(param))
|
||||||
|
else:
|
||||||
|
typed_params.append(param)
|
||||||
|
|
||||||
|
return method, typed_params
|
||||||
|
|
||||||
|
|
||||||
class HttpHandler(BaseHTTPRequestHandler):
|
class HttpHandler(BaseHTTPRequestHandler):
|
||||||
def page_info(self, info_str):
|
|
||||||
content = html_content_start('BasicSwap Info') \
|
|
||||||
+ '<p>Info: ' + info_str + '</p>' \
|
|
||||||
+ '<p><a href=\'/\'>home</a></p></body></html>'
|
|
||||||
return bytes(content, 'UTF-8')
|
|
||||||
|
|
||||||
def page_error(self, error_str):
|
def log_error(self, format, *args):
|
||||||
content = html_content_start('BasicSwap Error') \
|
super().log_message(format, *args)
|
||||||
+ '<p>Error: ' + error_str + '</p>' \
|
|
||||||
+ '<p><a href=\'/\'>home</a></p></body></html>'
|
def log_message(self, format, *args):
|
||||||
return bytes(content, 'UTF-8')
|
# TODO: Add debug flag to re-enable.
|
||||||
|
pass
|
||||||
|
|
||||||
|
def generate_form_id(self):
|
||||||
|
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 = urllib.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))
|
||||||
else:
|
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):
|
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_url'] = swap_client.ws_server.url
|
||||||
|
if swap_client.debug:
|
||||||
|
args_dict['debug_mode'] = True
|
||||||
|
if swap_client.debug_ui:
|
||||||
|
args_dict['debug_ui_mode'] = True
|
||||||
|
if swap_client.use_tor_proxy:
|
||||||
|
args_dict['use_tor_proxy'] = True
|
||||||
|
# TODO: Cache value?
|
||||||
|
try:
|
||||||
|
args_dict['tor_established'] = True if get_tor_established_state(swap_client) == '1' else False
|
||||||
|
except Exception:
|
||||||
|
if swap_client.debug:
|
||||||
|
swap_client.log.error(traceback.format_exc())
|
||||||
|
|
||||||
|
if swap_client._show_notifications:
|
||||||
|
args_dict['notifications'] = swap_client.getNotifications()
|
||||||
|
|
||||||
|
if 'messages' in args_dict:
|
||||||
|
messages_with_ids = []
|
||||||
|
for msg in args_dict['messages']:
|
||||||
|
messages_with_ids.append((self.server.msg_id_counter, msg))
|
||||||
|
self.server.msg_id_counter += 1
|
||||||
|
args_dict['messages'] = messages_with_ids
|
||||||
|
if 'err_messages' in args_dict:
|
||||||
|
err_messages_with_ids = []
|
||||||
|
for msg in args_dict['err_messages']:
|
||||||
|
err_messages_with_ids.append((self.server.msg_id_counter, msg))
|
||||||
|
self.server.msg_id_counter += 1
|
||||||
|
args_dict['err_messages'] = err_messages_with_ids
|
||||||
|
|
||||||
|
shutdown_token = os.urandom(8).hex()
|
||||||
|
self.server.session_tokens['shutdown'] = shutdown_token
|
||||||
|
args_dict['shutdown_token'] = shutdown_token
|
||||||
|
|
||||||
|
encrypted, locked = swap_client.getLockedState()
|
||||||
|
args_dict['encrypted'] = encrypted
|
||||||
|
args_dict['locked'] = locked
|
||||||
|
|
||||||
|
if self.server.msg_id_counter >= 0x7FFFFFFF:
|
||||||
|
self.server.msg_id_counter = 0
|
||||||
|
|
||||||
|
args_dict['version'] = version
|
||||||
|
|
||||||
|
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=os.urandom(8).hex(),
|
form_id=self.generate_form_id(),
|
||||||
**args_dict,
|
**args_dict,
|
||||||
), 'UTF-8')
|
), 'UTF-8')
|
||||||
|
|
||||||
|
def render_simple_template(self, template, args_dict):
|
||||||
|
swap_client = self.server.swap_client
|
||||||
|
return bytes(template.render(
|
||||||
|
title=self.server.title,
|
||||||
|
**args_dict,
|
||||||
|
), 'UTF-8')
|
||||||
|
|
||||||
|
def page_info(self, info_str, post_string=None):
|
||||||
|
template = env.get_template('info.html')
|
||||||
|
swap_client = self.server.swap_client
|
||||||
|
summary = swap_client.getSummary()
|
||||||
|
return self.render_template(template, {
|
||||||
|
'title_str': 'BasicSwap Info',
|
||||||
|
'message_str': info_str,
|
||||||
|
'summary': summary,
|
||||||
|
})
|
||||||
|
|
||||||
|
def page_error(self, error_str, post_string=None):
|
||||||
|
template = env.get_template('error.html')
|
||||||
|
swap_client = self.server.swap_client
|
||||||
|
summary = swap_client.getSummary()
|
||||||
|
return self.render_template(template, {
|
||||||
|
'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
|
||||||
|
swap_client.checkSystemStatus()
|
||||||
|
summary = swap_client.getSummary()
|
||||||
|
|
||||||
result = None
|
result = None
|
||||||
explorer = -1
|
explorer = -1
|
||||||
action = -1
|
action = -1
|
||||||
messages = []
|
messages = []
|
||||||
form_data = self.checkForm(post_string, 'explorers', messages)
|
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')
|
||||||
@@ -187,52 +249,76 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
template = env.get_template('explorers.html')
|
template = env.get_template('explorers.html')
|
||||||
return self.render_template(template, {
|
return self.render_template(template, {
|
||||||
|
'messages': messages,
|
||||||
|
'err_messages': err_messages,
|
||||||
'explorers': listAvailableExplorers(swap_client),
|
'explorers': listAvailableExplorers(swap_client),
|
||||||
'explorer': explorer,
|
'explorer': explorer,
|
||||||
'actions': listExplorerActions(swap_client),
|
'actions': listExplorerActions(swap_client),
|
||||||
'action': action,
|
'action': action,
|
||||||
'result': result
|
'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
|
||||||
|
swap_client.checkSystemStatus()
|
||||||
|
summary = swap_client.getSummary()
|
||||||
|
|
||||||
result = None
|
result = None
|
||||||
coin_type = -1
|
coin_type = -1
|
||||||
coin_id = -1
|
coin_id = -1
|
||||||
|
call_type = 'cli'
|
||||||
|
type_map = ''
|
||||||
messages = []
|
messages = []
|
||||||
form_data = self.checkForm(post_string, 'rpc', messages)
|
err_messages = []
|
||||||
|
form_data = self.checkForm(post_string, 'rpc', err_messages)
|
||||||
if form_data:
|
if form_data:
|
||||||
try:
|
try:
|
||||||
coin_id = int(form_data[b'coin_type'][0])
|
call_type = get_data_entry_or(form_data, 'call_type', 'cli')
|
||||||
if coin_id in (-2, -3, -4):
|
type_map = get_data_entry_or(form_data, 'type_map', '')
|
||||||
coin_type = Coins(Coins.XMR)
|
try:
|
||||||
else:
|
coin_id = int(get_data_entry(form_data, 'coin_type'))
|
||||||
coin_type = Coins(coin_id)
|
if coin_id in (-2, -3, -4):
|
||||||
except Exception:
|
coin_type = Coins(Coins.XMR)
|
||||||
raise ValueError('Unknown Coin Type')
|
elif coin_id in (-5,):
|
||||||
|
coin_type = Coins(Coins.LTC)
|
||||||
|
else:
|
||||||
|
coin_type = Coins(coin_id)
|
||||||
|
except Exception:
|
||||||
|
raise ValueError('Unknown Coin Type')
|
||||||
|
|
||||||
cmd = form_data[b'cmd'][0].decode('utf-8')
|
try:
|
||||||
|
cmd = get_data_entry(form_data, 'cmd')
|
||||||
try:
|
except Exception:
|
||||||
|
raise ValueError('Invalid command')
|
||||||
if coin_type == Coins.XMR:
|
if coin_type == Coins.XMR:
|
||||||
ci = swap_client.ci(coin_type)
|
ci = swap_client.ci(coin_type)
|
||||||
arr = cmd.split(None, 1)
|
arr = cmd.split(None, 1)
|
||||||
method = arr[0]
|
method = arr[0]
|
||||||
params = json.loads(arr[1]) if len(arr) > 1 else []
|
params = json.loads(arr[1]) if len(arr) > 1 else []
|
||||||
if coin_id == -4:
|
if coin_id == -4:
|
||||||
rv = ci.rpc_wallet_cb(method, params)
|
rv = ci.rpc_wallet(method, params)
|
||||||
elif coin_id == -3:
|
elif coin_id == -3:
|
||||||
rv = ci.rpc_cb(method, params)
|
rv = ci.rpc(method, params)
|
||||||
elif coin_id == -2:
|
elif coin_id == -2:
|
||||||
if params == []:
|
if params == []:
|
||||||
params = None
|
params = None
|
||||||
rv = ci.rpc_cb2(method, params)
|
rv = ci.rpc2(method, params)
|
||||||
else:
|
else:
|
||||||
raise ValueError('Unknown XMR RPC variant')
|
raise ValueError('Unknown XMR RPC variant')
|
||||||
result = json.dumps(rv, indent=4)
|
result = json.dumps(rv, indent=4)
|
||||||
else:
|
else:
|
||||||
result = cmd + '\n' + swap_client.callcoincli(coin_type, cmd)
|
if call_type == 'http':
|
||||||
|
method, params = parse_cmd(cmd, type_map)
|
||||||
|
if coin_id == -5:
|
||||||
|
rv = swap_client.ci(coin_type).rpc_wallet_mweb(method, params)
|
||||||
|
else:
|
||||||
|
rv = swap_client.ci(coin_type).rpc_wallet(method, params)
|
||||||
|
if not isinstance(rv, str):
|
||||||
|
rv = json.dumps(rv, indent=4)
|
||||||
|
result = cmd + '\n' + rv
|
||||||
|
else:
|
||||||
|
result = cmd + '\n' + swap_client.callcoincli(coin_type, cmd)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
result = str(ex)
|
result = str(ex)
|
||||||
if self.server.swap_client.debug is True:
|
if self.server.swap_client.debug is True:
|
||||||
@@ -241,375 +327,52 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
template = env.get_template('rpc.html')
|
template = env.get_template('rpc.html')
|
||||||
|
|
||||||
coins = listAvailableCoins(swap_client, with_variants=False)
|
coins = listAvailableCoins(swap_client, with_variants=False)
|
||||||
|
with_xmr: bool = any(c[0] == Coins.XMR for c in coins)
|
||||||
coins = [c for c in coins if c[0] != Coins.XMR]
|
coins = [c for c in coins if c[0] != Coins.XMR]
|
||||||
coins.append((-2, 'Monero'))
|
|
||||||
coins.append((-3, 'Monero JSON'))
|
if any(c[0] == Coins.LTC for c in coins):
|
||||||
coins.append((-4, 'Monero Wallet'))
|
coins.append((-5, 'Litecoin MWEB Wallet'))
|
||||||
|
if with_xmr:
|
||||||
|
coins.append((-2, 'Monero'))
|
||||||
|
coins.append((-3, 'Monero JSON'))
|
||||||
|
coins.append((-4, 'Monero Wallet'))
|
||||||
|
|
||||||
return self.render_template(template, {
|
return self.render_template(template, {
|
||||||
|
'messages': messages,
|
||||||
|
'err_messages': err_messages,
|
||||||
'coins': coins,
|
'coins': coins,
|
||||||
'coin_type': coin_id,
|
'coin_type': coin_id,
|
||||||
|
'call_type': call_type,
|
||||||
'result': result,
|
'result': result,
|
||||||
'messages': messages,
|
'messages': messages,
|
||||||
})
|
'summary': summary,
|
||||||
|
|
||||||
def page_debug(self, url_split, post_string):
|
|
||||||
swap_client = self.server.swap_client
|
|
||||||
|
|
||||||
result = None
|
|
||||||
messages = []
|
|
||||||
form_data = self.checkForm(post_string, 'wallets', messages)
|
|
||||||
if form_data:
|
|
||||||
if have_data_entry(form_data, 'reinit_xmr'):
|
|
||||||
try:
|
|
||||||
swap_client.initialiseWallet(Coins.XMR)
|
|
||||||
messages.append('Done.')
|
|
||||||
except Exception as a:
|
|
||||||
messages.append('Failed.')
|
|
||||||
|
|
||||||
template = env.get_template('debug.html')
|
|
||||||
return self.render_template(template, {
|
|
||||||
'messages': messages,
|
|
||||||
'result': result,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
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
|
||||||
|
swap_client.checkSystemStatus()
|
||||||
active_swaps = swap_client.listSwapsInProgress()
|
active_swaps = swap_client.listSwapsInProgress()
|
||||||
|
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(template, {
|
||||||
'refresh': 30,
|
'refresh': 30,
|
||||||
'active_swaps': [(s[0].hex(), s[1], strBidState(s[2]), strTxState(s[3]), strTxState(s[4])) for s in active_swaps],
|
'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_settings(self, url_split, post_string):
|
|
||||||
swap_client = self.server.swap_client
|
|
||||||
|
|
||||||
messages = []
|
|
||||||
form_data = self.checkForm(post_string, 'settings', messages)
|
|
||||||
if form_data:
|
|
||||||
for name, c in swap_client.settings['chainclients'].items():
|
|
||||||
if have_data_entry(form_data, 'apply_' + name):
|
|
||||||
data = {'lookups': get_data_entry(form_data, 'lookups_' + name)}
|
|
||||||
if name == 'monero':
|
|
||||||
data['fee_priority'] = int(get_data_entry(form_data, 'fee_priority_' + name))
|
|
||||||
data['manage_daemon'] = True if get_data_entry(form_data, 'managedaemon_' + name) == 'true' else False
|
|
||||||
data['rpchost'] = get_data_entry(form_data, 'rpchost_' + name)
|
|
||||||
data['rpcport'] = int(get_data_entry(form_data, 'rpcport_' + name))
|
|
||||||
data['remotedaemonurls'] = get_data_entry(form_data, 'remotedaemonurls_' + name)
|
|
||||||
data['automatically_select_daemon'] = True if get_data_entry(form_data, 'autosetdaemon_' + name) == 'true' else False
|
|
||||||
else:
|
|
||||||
data['conf_target'] = int(get_data_entry(form_data, 'conf_target_' + name))
|
|
||||||
if name == 'particl':
|
|
||||||
data['anon_tx_ring_size'] = int(get_data_entry(form_data, 'rct_ring_size_' + name))
|
|
||||||
|
|
||||||
settings_changed, suggest_reboot = swap_client.editSettings(name, data)
|
|
||||||
if settings_changed is True:
|
|
||||||
messages.append('Settings applied.')
|
|
||||||
if suggest_reboot is True:
|
|
||||||
messages.append('Please restart BasicSwap.')
|
|
||||||
elif have_data_entry(form_data, 'enable_' + name):
|
|
||||||
swap_client.enableCoin(name)
|
|
||||||
messages.append(name.capitalize() + ' enabled, shutting down.')
|
|
||||||
swap_client.stopRunning()
|
|
||||||
elif have_data_entry(form_data, 'disable_' + name):
|
|
||||||
swap_client.disableCoin(name)
|
|
||||||
messages.append(name.capitalize() + ' disabled, shutting down.')
|
|
||||||
swap_client.stopRunning()
|
|
||||||
chains_formatted = []
|
|
||||||
|
|
||||||
sorted_names = sorted(swap_client.settings['chainclients'].keys())
|
|
||||||
for name in sorted_names:
|
|
||||||
c = swap_client.settings['chainclients'][name]
|
|
||||||
chains_formatted.append({
|
|
||||||
'name': name,
|
|
||||||
'lookups': c.get('chain_lookups', 'local'),
|
|
||||||
'manage_daemon': c.get('manage_daemon', 'Unknown'),
|
|
||||||
'connection_type': c.get('connection_type', 'Unknown'),
|
|
||||||
})
|
|
||||||
if name == 'monero':
|
|
||||||
chains_formatted[-1]['fee_priority'] = c.get('fee_priority', 0)
|
|
||||||
chains_formatted[-1]['manage_wallet_daemon'] = c.get('manage_wallet_daemon', 'Unknown')
|
|
||||||
chains_formatted[-1]['rpchost'] = c.get('rpchost', 'localhost')
|
|
||||||
chains_formatted[-1]['rpcport'] = int(c.get('rpcport', 18081))
|
|
||||||
chains_formatted[-1]['remotedaemonurls'] = '\n'.join(c.get('remote_daemon_urls', []))
|
|
||||||
chains_formatted[-1]['autosetdaemon'] = c.get('automatically_select_daemon', False)
|
|
||||||
else:
|
|
||||||
chains_formatted[-1]['conf_target'] = c.get('conf_target', 2)
|
|
||||||
|
|
||||||
if name == 'particl':
|
|
||||||
chains_formatted[-1]['anon_tx_ring_size'] = c.get('anon_tx_ring_size', 12)
|
|
||||||
else:
|
|
||||||
if c.get('connection_type', 'Unknown') == 'none':
|
|
||||||
if 'connection_type_prev' in c:
|
|
||||||
chains_formatted[-1]['can_reenable'] = True
|
|
||||||
else:
|
|
||||||
chains_formatted[-1]['can_disable'] = True
|
|
||||||
|
|
||||||
template = env.get_template('settings.html')
|
|
||||||
return self.render_template(template, {
|
|
||||||
'messages': messages,
|
|
||||||
'chains': chains_formatted,
|
|
||||||
})
|
|
||||||
|
|
||||||
def page_bid(self, url_split, post_string):
|
|
||||||
ensure(len(url_split) > 2, 'Bid ID not specified')
|
|
||||||
try:
|
|
||||||
bid_id = bytes.fromhex(url_split[2])
|
|
||||||
assert len(bid_id) == 28
|
|
||||||
except Exception:
|
|
||||||
raise ValueError('Bad bid ID')
|
|
||||||
swap_client = self.server.swap_client
|
|
||||||
|
|
||||||
messages = []
|
|
||||||
show_txns = False
|
|
||||||
show_offerer_seq_diagram = False
|
|
||||||
show_bidder_seq_diagram = False
|
|
||||||
show_lock_transfers = False
|
|
||||||
edit_bid = False
|
|
||||||
view_tx_ind = None
|
|
||||||
form_data = self.checkForm(post_string, 'bid', messages)
|
|
||||||
if form_data:
|
|
||||||
if b'abandon_bid' in form_data:
|
|
||||||
try:
|
|
||||||
swap_client.abandonBid(bid_id)
|
|
||||||
messages.append('Bid abandoned')
|
|
||||||
except Exception as ex:
|
|
||||||
messages.append('Abandon failed ' + str(ex))
|
|
||||||
elif b'accept_bid' in form_data:
|
|
||||||
try:
|
|
||||||
swap_client.acceptBid(bid_id)
|
|
||||||
messages.append('Bid accepted')
|
|
||||||
except Exception as ex:
|
|
||||||
messages.append('Accept failed ' + str(ex))
|
|
||||||
elif b'show_txns' in form_data:
|
|
||||||
show_txns = True
|
|
||||||
elif b'show_offerer_seq_diagram' in form_data:
|
|
||||||
show_offerer_seq_diagram = True
|
|
||||||
elif b'show_bidder_seq_diagram' in form_data:
|
|
||||||
show_bidder_seq_diagram = True
|
|
||||||
elif b'edit_bid' in form_data:
|
|
||||||
edit_bid = True
|
|
||||||
elif b'edit_bid_submit' in form_data:
|
|
||||||
data = {
|
|
||||||
'bid_state': int(form_data[b'new_state'][0]),
|
|
||||||
'debug_ind': int(get_data_entry_or(form_data, 'debugind', -1)),
|
|
||||||
'kbs_other': get_data_entry_or(form_data, 'kbs_other', None),
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
swap_client.manualBidUpdate(bid_id, data)
|
|
||||||
messages.append('Bid edited')
|
|
||||||
except Exception as ex:
|
|
||||||
messages.append('Edit failed ' + str(ex))
|
|
||||||
elif b'view_tx_submit' in form_data:
|
|
||||||
show_txns = True
|
|
||||||
view_tx_ind = form_data[b'view_tx'][0].decode('utf-8')
|
|
||||||
elif b'view_lock_transfers' in form_data:
|
|
||||||
show_txns = True
|
|
||||||
show_lock_transfers = True
|
|
||||||
|
|
||||||
bid, xmr_swap, offer, xmr_offer, events = swap_client.getXmrBidAndOffer(bid_id)
|
|
||||||
ensure(bid, 'Unknown bid ID')
|
|
||||||
|
|
||||||
data = describeBid(swap_client, bid, xmr_swap, offer, xmr_offer, events, edit_bid, show_txns, view_tx_ind, show_lock_transfers=show_lock_transfers)
|
|
||||||
|
|
||||||
if bid.debug_ind is not None and bid.debug_ind > 0:
|
|
||||||
messages.append('Debug flag set: {}, {}'.format(bid.debug_ind, DebugTypes(bid.debug_ind).name))
|
|
||||||
|
|
||||||
data['show_bidder_seq_diagram'] = show_bidder_seq_diagram
|
|
||||||
data['show_offerer_seq_diagram'] = show_offerer_seq_diagram
|
|
||||||
|
|
||||||
old_states = listOldBidStates(bid)
|
|
||||||
|
|
||||||
if len(data['addr_from_label']) > 0:
|
|
||||||
data['addr_from_label'] = '(' + data['addr_from_label'] + ')'
|
|
||||||
|
|
||||||
template = env.get_template('bid_xmr.html') if offer.swap_type == SwapTypes.XMR_SWAP else env.get_template('bid.html')
|
|
||||||
return self.render_template(template, {
|
|
||||||
'bid_id': bid_id.hex(),
|
|
||||||
'messages': messages,
|
|
||||||
'data': data,
|
|
||||||
'edit_bid': edit_bid,
|
|
||||||
'old_states': old_states,
|
|
||||||
})
|
|
||||||
|
|
||||||
def page_bids(self, url_split, post_string, sent=False, available=False):
|
|
||||||
swap_client = self.server.swap_client
|
|
||||||
|
|
||||||
filters = {
|
|
||||||
'page_no': 1,
|
|
||||||
'bid_state_ind': -1,
|
|
||||||
'with_expired': True,
|
|
||||||
'limit': PAGE_LIMIT,
|
|
||||||
'sort_by': 'created_at',
|
|
||||||
'sort_dir': 'desc',
|
|
||||||
}
|
|
||||||
if available:
|
|
||||||
filters['bid_state_ind'] = BidStates.BID_RECEIVED
|
|
||||||
filters['with_expired'] = False
|
|
||||||
|
|
||||||
messages = []
|
|
||||||
form_data = self.checkForm(post_string, 'bids', messages)
|
|
||||||
if form_data and have_data_entry(form_data, 'applyfilters'):
|
|
||||||
if have_data_entry(form_data, 'sort_by'):
|
|
||||||
sort_by = get_data_entry(form_data, 'sort_by')
|
|
||||||
ensure(sort_by in ['created_at', ], 'Invalid sort by')
|
|
||||||
filters['sort_by'] = sort_by
|
|
||||||
if have_data_entry(form_data, 'sort_dir'):
|
|
||||||
sort_dir = get_data_entry(form_data, 'sort_dir')
|
|
||||||
ensure(sort_dir in ['asc', 'desc'], 'Invalid sort dir')
|
|
||||||
filters['sort_dir'] = sort_dir
|
|
||||||
if have_data_entry(form_data, 'state'):
|
|
||||||
state_ind = get_data_entry(form_data, 'state')
|
|
||||||
if state_ind != -1:
|
|
||||||
try:
|
|
||||||
state = BidStates(state_ind)
|
|
||||||
except Exception:
|
|
||||||
raise ValueError('Invalid state')
|
|
||||||
filters['bid_state_ind'] = state_ind
|
|
||||||
if have_data_entry(form_data, 'with_expired'):
|
|
||||||
with_expired = toBool(get_data_entry(form_data, 'with_expired'))
|
|
||||||
filters['with_expired'] = with_expired
|
|
||||||
|
|
||||||
set_pagination_filters(form_data, filters)
|
|
||||||
|
|
||||||
bids = swap_client.listBids(sent=sent, filters=filters)
|
|
||||||
|
|
||||||
page_data = {
|
|
||||||
'bid_states': listBidStates()
|
|
||||||
}
|
|
||||||
|
|
||||||
template = env.get_template('bids.html')
|
|
||||||
return self.render_template(template, {
|
|
||||||
'page_type': 'Sent' if sent else 'Received',
|
|
||||||
'messages': messages,
|
|
||||||
'filters': filters,
|
|
||||||
'data': page_data,
|
|
||||||
'bids': [(format_timestamp(b[0]),
|
|
||||||
b[2].hex(), b[3].hex(), strBidState(b[5]), strTxState(b[7]), strTxState(b[8]), b[11]) for b in bids],
|
|
||||||
})
|
})
|
||||||
|
|
||||||
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
|
||||||
|
swap_client.checkSystemStatus()
|
||||||
watched_outputs, last_scanned = swap_client.listWatchedOutputs()
|
watched_outputs, last_scanned = swap_client.listWatchedOutputs()
|
||||||
|
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(template, {
|
||||||
'refresh': 30,
|
'refresh': 30,
|
||||||
'last_scanned': [(getCoinName(ls[0]), ls[1]) for ls in last_scanned],
|
'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],
|
'watched_outputs': [(wo[1].hex(), getCoinName(wo[0]), wo[2], wo[3], int(wo[4])) for wo in watched_outputs],
|
||||||
})
|
'summary': summary,
|
||||||
|
|
||||||
def page_smsgaddresses(self, url_split, post_string):
|
|
||||||
swap_client = self.server.swap_client
|
|
||||||
|
|
||||||
page_data = {}
|
|
||||||
messages = []
|
|
||||||
smsgaddresses = []
|
|
||||||
|
|
||||||
listaddresses = True
|
|
||||||
form_data = self.checkForm(post_string, 'smsgaddresses', messages)
|
|
||||||
if form_data:
|
|
||||||
edit_address_id = None
|
|
||||||
for key in form_data:
|
|
||||||
if key.startswith(b'editaddr_'):
|
|
||||||
edit_address_id = int(key.split(b'_')[1])
|
|
||||||
break
|
|
||||||
if edit_address_id is not None:
|
|
||||||
listaddresses = False
|
|
||||||
page_data['edit_address'] = edit_address_id
|
|
||||||
page_data['addr_data'] = swap_client.listAllSMSGAddresses(addr_id=edit_address_id)[0]
|
|
||||||
elif b'saveaddr' in form_data:
|
|
||||||
edit_address_id = int(form_data[b'edit_address_id'][0].decode('utf-8'))
|
|
||||||
edit_addr = form_data[b'edit_address'][0].decode('utf-8')
|
|
||||||
active_ind = int(form_data[b'active_ind'][0].decode('utf-8'))
|
|
||||||
ensure(active_ind in (0, 1), 'Invalid sort by')
|
|
||||||
addressnote = '' if b'addressnote' not in form_data else form_data[b'addressnote'][0].decode('utf-8')
|
|
||||||
if not validateTextInput(addressnote, 'Address note', messages, max_length=30):
|
|
||||||
listaddresses = False
|
|
||||||
page_data['edit_address'] = edit_address_id
|
|
||||||
else:
|
|
||||||
swap_client.editSMSGAddress(edit_addr, active_ind=active_ind, addressnote=addressnote)
|
|
||||||
messages.append(f'Edited address {edit_addr}')
|
|
||||||
elif b'shownewaddr' in form_data:
|
|
||||||
listaddresses = False
|
|
||||||
page_data['new_address'] = True
|
|
||||||
elif b'showaddaddr' in form_data:
|
|
||||||
listaddresses = False
|
|
||||||
page_data['new_send_address'] = True
|
|
||||||
elif b'createnewaddr' in form_data:
|
|
||||||
addressnote = '' if b'addressnote' not in form_data else form_data[b'addressnote'][0].decode('utf-8')
|
|
||||||
if not validateTextInput(addressnote, 'Address note', messages, max_length=30):
|
|
||||||
listaddresses = False
|
|
||||||
page_data['new_address'] = True
|
|
||||||
else:
|
|
||||||
new_addr, pubkey = swap_client.newSMSGAddress(addressnote=addressnote)
|
|
||||||
messages.append(f'Created address {new_addr}, pubkey {pubkey}')
|
|
||||||
elif b'createnewsendaddr' in form_data:
|
|
||||||
pubkey_hex = form_data[b'addresspubkey'][0].decode('utf-8')
|
|
||||||
addressnote = '' if b'addressnote' not in form_data else form_data[b'addressnote'][0].decode('utf-8')
|
|
||||||
if not validateTextInput(addressnote, 'Address note', messages, max_length=30) or \
|
|
||||||
not validateTextInput(pubkey_hex, 'Pubkey', messages, max_length=66):
|
|
||||||
listaddresses = False
|
|
||||||
page_data['new_send_address'] = True
|
|
||||||
else:
|
|
||||||
new_addr = swap_client.addSMSGAddress(pubkey_hex, addressnote=addressnote)
|
|
||||||
messages.append(f'Added address {new_addr}')
|
|
||||||
|
|
||||||
if listaddresses is True:
|
|
||||||
smsgaddresses = swap_client.listAllSMSGAddresses()
|
|
||||||
network_addr = swap_client.network_addr
|
|
||||||
|
|
||||||
for addr in smsgaddresses:
|
|
||||||
addr['type'] = strAddressType(addr['type'])
|
|
||||||
|
|
||||||
template = env.get_template('smsgaddresses.html')
|
|
||||||
return self.render_template(template, {
|
|
||||||
'messages': messages,
|
|
||||||
'data': page_data,
|
|
||||||
'smsgaddresses': smsgaddresses,
|
|
||||||
'network_addr': network_addr,
|
|
||||||
})
|
|
||||||
|
|
||||||
def page_identity(self, url_split, post_string):
|
|
||||||
ensure(len(url_split) > 2, 'Address not specified')
|
|
||||||
identity_address = url_split[2]
|
|
||||||
swap_client = self.server.swap_client
|
|
||||||
|
|
||||||
page_data = {'identity_address': identity_address}
|
|
||||||
messages = []
|
|
||||||
form_data = self.checkForm(post_string, 'identity', messages)
|
|
||||||
if form_data:
|
|
||||||
if have_data_entry(form_data, 'edit'):
|
|
||||||
page_data['show_edit_form'] = True
|
|
||||||
if have_data_entry(form_data, 'apply'):
|
|
||||||
new_label = get_data_entry(form_data, 'label')
|
|
||||||
|
|
||||||
try:
|
|
||||||
swap_client.updateIdentity(identity_address, new_label)
|
|
||||||
messages.append('Updated')
|
|
||||||
except Exception as e:
|
|
||||||
messages.append('Error')
|
|
||||||
|
|
||||||
try:
|
|
||||||
identity = swap_client.getIdentity(identity_address)
|
|
||||||
if identity is None:
|
|
||||||
raise ValueError('Unknown address')
|
|
||||||
page_data['label'] = identity.label
|
|
||||||
page_data['num_sent_bids_successful'] = identity.num_sent_bids_successful
|
|
||||||
page_data['num_recv_bids_successful'] = identity.num_recv_bids_successful
|
|
||||||
page_data['num_sent_bids_rejected'] = identity.num_sent_bids_rejected
|
|
||||||
page_data['num_recv_bids_rejected'] = identity.num_recv_bids_rejected
|
|
||||||
page_data['num_sent_bids_failed'] = identity.num_sent_bids_failed
|
|
||||||
page_data['num_recv_bids_failed'] = identity.num_recv_bids_failed
|
|
||||||
except Exception as e:
|
|
||||||
messages.append(e)
|
|
||||||
|
|
||||||
template = env.get_template('identity.html')
|
|
||||||
return self.render_template(template, {
|
|
||||||
'messages': messages,
|
|
||||||
'data': page_data,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
def page_shutdown(self, url_split, post_string):
|
def page_shutdown(self, url_split, post_string):
|
||||||
@@ -627,25 +390,22 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
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()
|
||||||
summary = swap_client.getSummary()
|
summary = swap_client.getSummary()
|
||||||
|
|
||||||
shutdown_token = os.urandom(8).hex()
|
|
||||||
self.server.session_tokens['shutdown'] = shutdown_token
|
|
||||||
|
|
||||||
template = env.get_template('index.html')
|
template = env.get_template('index.html')
|
||||||
return self.render_template(template, {
|
return self.render_template(template, {
|
||||||
'refresh': 30,
|
'refresh': 30,
|
||||||
'version': __version__,
|
|
||||||
'summary': summary,
|
'summary': summary,
|
||||||
'use_tor_proxy': swap_client.use_tor_proxy,
|
'use_tor_proxy': swap_client.use_tor_proxy
|
||||||
'shutdown_token': shutdown_token
|
|
||||||
})
|
})
|
||||||
|
|
||||||
def page_404(self, url_split):
|
def page_404(self, url_split):
|
||||||
|
swap_client = self.server.swap_client
|
||||||
|
summary = swap_client.getSummary()
|
||||||
template = env.get_template('404.html')
|
template = env.get_template('404.html')
|
||||||
return bytes(template.render(
|
return self.render_template(template, {
|
||||||
title=self.server.title,
|
'summary': summary,
|
||||||
), 'UTF-8')
|
})
|
||||||
|
|
||||||
def putHeaders(self, status_code, content_type):
|
def putHeaders(self, status_code, content_type):
|
||||||
self.send_response(status_code)
|
self.send_response(status_code)
|
||||||
@@ -655,39 +415,24 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
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):
|
||||||
parsed = urllib.parse.urlparse(self.path)
|
swap_client = self.server.swap_client
|
||||||
|
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_index
|
func = js_url_to_function(url_split)
|
||||||
if len(url_split) > 2:
|
|
||||||
func = {
|
|
||||||
'coins': js_coins,
|
|
||||||
'wallets': js_wallets,
|
|
||||||
'offers': js_offers,
|
|
||||||
'sentoffers': js_sentoffers,
|
|
||||||
'bids': js_bids,
|
|
||||||
'sentbids': js_sentbids,
|
|
||||||
'network': js_network,
|
|
||||||
'revokeoffer': js_revokeoffer,
|
|
||||||
'smsgaddresses': js_smsgaddresses,
|
|
||||||
'rate': js_rate,
|
|
||||||
'rates': js_rates,
|
|
||||||
'rateslist': js_rates_list,
|
|
||||||
}.get(url_split[2], js_index)
|
|
||||||
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:
|
||||||
if self.server.swap_client.debug is True:
|
if swap_client.debug is True:
|
||||||
self.server.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(os.path.join(static_path, 'sequence_diagrams', url_split[3]), 'rb') as fp:
|
||||||
self.putHeaders(status_code, 'image/svg+xml')
|
self.putHeaders(status_code, 'image/svg+xml')
|
||||||
@@ -699,6 +444,8 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
'.svg': 'image/svg+xml',
|
'.svg': 'image/svg+xml',
|
||||||
'.png': 'image/png',
|
'.png': 'image/png',
|
||||||
'.jpg': 'image/jpeg',
|
'.jpg': 'image/jpeg',
|
||||||
|
'.gif': 'image/gif',
|
||||||
|
'.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)
|
||||||
@@ -716,21 +463,18 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
self.putHeaders(status_code, 'application/javascript')
|
self.putHeaders(status_code, 'application/javascript')
|
||||||
return fp.read()
|
return fp.read()
|
||||||
else:
|
else:
|
||||||
self.putHeaders(status_code, 'text/html')
|
|
||||||
return self.page_404(url_split)
|
return self.page_404(url_split)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
self.putHeaders(status_code, 'text/html')
|
|
||||||
return self.page_404(url_split)
|
return self.page_404(url_split)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
if self.server.swap_client.debug is True:
|
if swap_client.debug is True:
|
||||||
self.server.swap_client.log.error(traceback.format_exc())
|
swap_client.log.error(traceback.format_exc())
|
||||||
self.putHeaders(status_code, 'text/html')
|
|
||||||
return self.page_error(str(ex))
|
return self.page_error(str(ex))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.putHeaders(status_code, 'text/html')
|
|
||||||
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':
|
||||||
@@ -738,11 +482,15 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
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 self.page_settings(url_split, post_string)
|
return page_settings(self, url_split, post_string)
|
||||||
|
if page == 'error':
|
||||||
|
return self.page_error(url_split, post_string)
|
||||||
|
if page == 'info':
|
||||||
|
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 self.page_debug(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':
|
||||||
@@ -754,19 +502,19 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
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 self.page_bid(url_split, post_string)
|
return page_bid(self, url_split, post_string)
|
||||||
if page == 'bids':
|
if page == 'receivedbids':
|
||||||
return self.page_bids(url_split, post_string)
|
return page_bids(self, url_split, post_string, received=True)
|
||||||
if page == 'sentbids':
|
if page == 'sentbids':
|
||||||
return self.page_bids(url_split, post_string, sent=True)
|
return page_bids(self, url_split, post_string, sent=True)
|
||||||
if page == 'availablebids':
|
if page == 'availablebids':
|
||||||
return self.page_bids(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 self.page_smsgaddresses(url_split, post_string)
|
return page_smsgaddresses(self, url_split, post_string)
|
||||||
if page == 'identity':
|
if page == 'identity':
|
||||||
return self.page_identity(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':
|
||||||
@@ -777,10 +525,20 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
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':
|
||||||
|
return page_changepassword(self, url_split, post_string)
|
||||||
|
if page == 'unlock':
|
||||||
|
return page_unlock(self, url_split, post_string)
|
||||||
|
if page == 'lock':
|
||||||
|
return page_lock(self, url_split, post_string)
|
||||||
|
if page != '':
|
||||||
|
return self.page_404(url_split)
|
||||||
return self.page_index(url_split)
|
return self.page_index(url_split)
|
||||||
|
except LockedCoinError:
|
||||||
|
return page_unlock(self, url_split, post_string)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
if self.server.swap_client.debug is True:
|
if swap_client.debug is True:
|
||||||
self.server.swap_client.log.error(traceback.format_exc())
|
swap_client.log.error(traceback.format_exc())
|
||||||
return self.page_error(str(ex))
|
return self.page_error(str(ex))
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
@@ -815,10 +573,11 @@ 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, ' + self.swap_client.chain
|
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
|
||||||
|
self.msg_id_counter = 0
|
||||||
|
|
||||||
self.timeout = 60
|
self.timeout = 60
|
||||||
HTTPServer.__init__(self, (self.host_name, self.port_no), HttpHandler)
|
HTTPServer.__init__(self, (self.host_name, self.port_no), HttpHandler)
|
||||||
@@ -834,11 +593,8 @@ class HttpThread(threading.Thread, HTTPServer):
|
|||||||
data = response.read()
|
data = response.read()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def stopped(self):
|
|
||||||
return self.stop_event.is_set()
|
|
||||||
|
|
||||||
def serve_forever(self):
|
def serve_forever(self):
|
||||||
while not self.stopped():
|
while not self.stop_event.is_set():
|
||||||
self.handle_request()
|
self.handle_request()
|
||||||
self.socket.close()
|
self.socket.close()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2023 tecnovert
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
|
|
||||||
|
class Curves(IntEnum):
|
||||||
|
secp256k1 = 1
|
||||||
|
ed25519 = 2
|
||||||
|
|||||||
0
basicswap/interface/contrib/__init__.py
Normal file
191
basicswap/interface/contrib/firo_test_framework/authproxy.py
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2011 Jeff Garzik
|
||||||
|
|
||||||
|
AuthServiceProxy has the following improvements over python-jsonrpc's
|
||||||
|
ServiceProxy class:
|
||||||
|
|
||||||
|
- HTTP connections persist for the life of the AuthServiceProxy object
|
||||||
|
(if server supports HTTP/1.1)
|
||||||
|
- sends protocol 'version', per JSON-RPC 1.1
|
||||||
|
- sends proper, incrementing 'id'
|
||||||
|
- sends Basic HTTP authentication headers
|
||||||
|
- parses all JSON numbers that look like floats as Decimal
|
||||||
|
- uses standard Python json lib
|
||||||
|
|
||||||
|
Previous copyright, from python-jsonrpc/jsonrpc/proxy.py:
|
||||||
|
|
||||||
|
Copyright (c) 2007 Jan-Klaas Kollhof
|
||||||
|
|
||||||
|
This file is part of jsonrpc.
|
||||||
|
|
||||||
|
jsonrpc is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2.1 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This software is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with this software; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
import http.client as httplib
|
||||||
|
except ImportError:
|
||||||
|
import httplib
|
||||||
|
import base64
|
||||||
|
import decimal
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
try:
|
||||||
|
import urllib.parse as urlparse
|
||||||
|
except ImportError:
|
||||||
|
import urlparse
|
||||||
|
|
||||||
|
USER_AGENT = "AuthServiceProxy/0.1"
|
||||||
|
|
||||||
|
HTTP_TIMEOUT = 30
|
||||||
|
|
||||||
|
log = logging.getLogger("BitcoinRPC")
|
||||||
|
|
||||||
|
class JSONRPCException(Exception):
|
||||||
|
def __init__(self, rpc_error):
|
||||||
|
try:
|
||||||
|
errmsg = '%(message)s (%(code)i)' % rpc_error
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
errmsg = ''
|
||||||
|
Exception.__init__(self, errmsg)
|
||||||
|
self.error = rpc_error
|
||||||
|
|
||||||
|
|
||||||
|
def EncodeDecimal(o):
|
||||||
|
if isinstance(o, decimal.Decimal):
|
||||||
|
return str(o)
|
||||||
|
raise TypeError(repr(o) + " is not JSON serializable")
|
||||||
|
|
||||||
|
class AuthServiceProxy(object):
|
||||||
|
__id_count = 0
|
||||||
|
|
||||||
|
# ensure_ascii: escape unicode as \uXXXX, passed to json.dumps
|
||||||
|
def __init__(self, service_url, service_name=None, timeout=HTTP_TIMEOUT, connection=None, ensure_ascii=True):
|
||||||
|
self.__service_url = service_url
|
||||||
|
self._service_name = service_name
|
||||||
|
self.ensure_ascii = ensure_ascii # can be toggled on the fly by tests
|
||||||
|
self.__url = urlparse.urlparse(service_url)
|
||||||
|
if self.__url.port is None:
|
||||||
|
port = 80
|
||||||
|
else:
|
||||||
|
port = self.__url.port
|
||||||
|
(user, passwd) = (self.__url.username, self.__url.password)
|
||||||
|
try:
|
||||||
|
user = user.encode('utf8')
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
passwd = passwd.encode('utf8')
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
authpair = user + b':' + passwd
|
||||||
|
self.__auth_header = b'Basic ' + base64.b64encode(authpair)
|
||||||
|
|
||||||
|
if connection:
|
||||||
|
# Callables re-use the connection of the original proxy
|
||||||
|
self.__conn = connection
|
||||||
|
elif self.__url.scheme == 'https':
|
||||||
|
self.__conn = httplib.HTTPSConnection(self.__url.hostname, port,
|
||||||
|
timeout=timeout)
|
||||||
|
else:
|
||||||
|
self.__conn = httplib.HTTPConnection(self.__url.hostname, port,
|
||||||
|
timeout=timeout)
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
if name.startswith('__') and name.endswith('__'):
|
||||||
|
# Python internal stuff
|
||||||
|
raise AttributeError
|
||||||
|
if self._service_name is not None:
|
||||||
|
name = "%s.%s" % (self._service_name, name)
|
||||||
|
return AuthServiceProxy(self.__service_url, name, connection=self.__conn)
|
||||||
|
|
||||||
|
def _request(self, method, path, postdata):
|
||||||
|
'''
|
||||||
|
Do a HTTP request, with retry if we get disconnected (e.g. due to a timeout).
|
||||||
|
This is a workaround for https://bugs.python.org/issue3566 which is fixed in Python 3.5.
|
||||||
|
'''
|
||||||
|
headers = {'Host': self.__url.hostname,
|
||||||
|
'User-Agent': USER_AGENT,
|
||||||
|
'Authorization': self.__auth_header,
|
||||||
|
'Content-type': 'application/json'}
|
||||||
|
try:
|
||||||
|
self.__conn.request(method, path, postdata, headers)
|
||||||
|
return self._get_response()
|
||||||
|
except httplib.BadStatusLine as e:
|
||||||
|
if e.line == "''": # if connection was closed, try again
|
||||||
|
self.__conn.close()
|
||||||
|
self.__conn.request(method, path, postdata, headers)
|
||||||
|
return self._get_response()
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
except (BrokenPipeError,ConnectionResetError):
|
||||||
|
# Python 3.5+ raises BrokenPipeError instead of BadStatusLine when the connection was reset
|
||||||
|
# ConnectionResetError happens on FreeBSD with Python 3.4
|
||||||
|
self.__conn.close()
|
||||||
|
self.__conn.request(method, path, postdata, headers)
|
||||||
|
return self._get_response()
|
||||||
|
|
||||||
|
def __call__(self, *args, **argsn):
|
||||||
|
AuthServiceProxy.__id_count += 1
|
||||||
|
|
||||||
|
log.debug("-%s-> %s %s"%(AuthServiceProxy.__id_count, self._service_name,
|
||||||
|
json.dumps(args, default=EncodeDecimal, ensure_ascii=self.ensure_ascii)))
|
||||||
|
if args and argsn:
|
||||||
|
raise ValueError('Cannot handle both named and positional arguments')
|
||||||
|
postdata = json.dumps({'version': '1.1',
|
||||||
|
'method': self._service_name,
|
||||||
|
'params': args or argsn,
|
||||||
|
'id': AuthServiceProxy.__id_count}, default=EncodeDecimal, ensure_ascii=self.ensure_ascii)
|
||||||
|
response = self._request('POST', self.__url.path, postdata.encode('utf-8'))
|
||||||
|
if response['error'] is not None:
|
||||||
|
raise JSONRPCException(response['error'])
|
||||||
|
elif 'result' not in response:
|
||||||
|
raise JSONRPCException({
|
||||||
|
'code': -343, 'message': 'missing JSON-RPC result'})
|
||||||
|
else:
|
||||||
|
return response['result']
|
||||||
|
|
||||||
|
def _batch(self, rpc_call_list):
|
||||||
|
postdata = json.dumps(list(rpc_call_list), default=EncodeDecimal, ensure_ascii=self.ensure_ascii)
|
||||||
|
log.debug("--> "+postdata)
|
||||||
|
return self._request('POST', self.__url.path, postdata.encode('utf-8'))
|
||||||
|
|
||||||
|
def _get_response(self):
|
||||||
|
try:
|
||||||
|
http_response = self.__conn.getresponse()
|
||||||
|
except socket.timeout as e:
|
||||||
|
raise JSONRPCException({
|
||||||
|
'code': -344,
|
||||||
|
'message': '%r RPC took longer than %f seconds. Consider '
|
||||||
|
'using larger timeout for calls that take '
|
||||||
|
'longer to return.' % (self._service_name,
|
||||||
|
self.__conn.timeout)})
|
||||||
|
if http_response is None:
|
||||||
|
raise JSONRPCException({
|
||||||
|
'code': -342, 'message': 'missing HTTP response from server'})
|
||||||
|
|
||||||
|
content_type = http_response.getheader('Content-Type')
|
||||||
|
if content_type != 'application/json':
|
||||||
|
raise JSONRPCException({
|
||||||
|
'code': -342, 'message': 'non-JSON HTTP response with \'%i %s\' from server' % (http_response.status, http_response.reason)})
|
||||||
|
|
||||||
|
responsedata = http_response.read().decode('utf8')
|
||||||
|
response = json.loads(responsedata, parse_float=decimal.Decimal)
|
||||||
|
if "error" in response and response["error"] is None:
|
||||||
|
log.debug("<-%s- %s"%(response["id"], json.dumps(response["result"], default=EncodeDecimal, ensure_ascii=self.ensure_ascii)))
|
||||||
|
else:
|
||||||
|
log.debug("<-- "+responsedata)
|
||||||
|
return response
|
||||||
101
basicswap/interface/contrib/firo_test_framework/bignum.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# bignum.py
|
||||||
|
#
|
||||||
|
# This file is copied from python-bitcoinlib.
|
||||||
|
#
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
#
|
||||||
|
|
||||||
|
"""Bignum routines"""
|
||||||
|
|
||||||
|
|
||||||
|
import struct
|
||||||
|
|
||||||
|
|
||||||
|
# generic big endian MPI format
|
||||||
|
|
||||||
|
def bn_bytes(v, have_ext=False):
|
||||||
|
ext = 0
|
||||||
|
if have_ext:
|
||||||
|
ext = 1
|
||||||
|
return ((v.bit_length()+7)//8) + ext
|
||||||
|
|
||||||
|
def bn2bin(v):
|
||||||
|
s = bytearray()
|
||||||
|
i = bn_bytes(v)
|
||||||
|
while i > 0:
|
||||||
|
s.append((v >> ((i-1) * 8)) & 0xff)
|
||||||
|
i -= 1
|
||||||
|
return s
|
||||||
|
|
||||||
|
def bin2bn(s):
|
||||||
|
l = 0
|
||||||
|
for ch in s:
|
||||||
|
l = (l << 8) | ch
|
||||||
|
return l
|
||||||
|
|
||||||
|
def bn2mpi(v):
|
||||||
|
have_ext = False
|
||||||
|
if v.bit_length() > 0:
|
||||||
|
have_ext = (v.bit_length() & 0x07) == 0
|
||||||
|
|
||||||
|
neg = False
|
||||||
|
if v < 0:
|
||||||
|
neg = True
|
||||||
|
v = -v
|
||||||
|
|
||||||
|
s = struct.pack(b">I", bn_bytes(v, have_ext))
|
||||||
|
ext = bytearray()
|
||||||
|
if have_ext:
|
||||||
|
ext.append(0)
|
||||||
|
v_bin = bn2bin(v)
|
||||||
|
if neg:
|
||||||
|
if have_ext:
|
||||||
|
ext[0] |= 0x80
|
||||||
|
else:
|
||||||
|
v_bin[0] |= 0x80
|
||||||
|
return s + ext + v_bin
|
||||||
|
|
||||||
|
def mpi2bn(s):
|
||||||
|
if len(s) < 4:
|
||||||
|
return None
|
||||||
|
s_size = bytes(s[:4])
|
||||||
|
v_len = struct.unpack(b">I", s_size)[0]
|
||||||
|
if len(s) != (v_len + 4):
|
||||||
|
return None
|
||||||
|
if v_len == 0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
v_str = bytearray(s[4:])
|
||||||
|
neg = False
|
||||||
|
i = v_str[0]
|
||||||
|
if i & 0x80:
|
||||||
|
neg = True
|
||||||
|
i &= ~0x80
|
||||||
|
v_str[0] = i
|
||||||
|
|
||||||
|
v = bin2bn(v_str)
|
||||||
|
|
||||||
|
if neg:
|
||||||
|
return -v
|
||||||
|
return v
|
||||||
|
|
||||||
|
# bitcoin-specific little endian format, with implicit size
|
||||||
|
def mpi2vch(s):
|
||||||
|
r = s[4:] # strip size
|
||||||
|
r = r[::-1] # reverse string, converting BE->LE
|
||||||
|
return r
|
||||||
|
|
||||||
|
def bn2vch(v):
|
||||||
|
return bytes(mpi2vch(bn2mpi(v)))
|
||||||
|
|
||||||
|
def vch2mpi(s):
|
||||||
|
r = struct.pack(b">I", len(s)) # size
|
||||||
|
r += s[::-1] # reverse string, converting LE->BE
|
||||||
|
return r
|
||||||
|
|
||||||
|
def vch2bn(s):
|
||||||
|
return mpi2bn(vch2mpi(s))
|
||||||
|
|
||||||
106
basicswap/interface/contrib/firo_test_framework/coverage.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright (c) 2015-2016 The Bitcoin Core developers
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
|
"""
|
||||||
|
This module contains utilities for doing coverage analysis on the RPC
|
||||||
|
interface.
|
||||||
|
|
||||||
|
It provides a way to track which RPC commands are exercised during
|
||||||
|
testing.
|
||||||
|
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
REFERENCE_FILENAME = 'rpc_interface.txt'
|
||||||
|
|
||||||
|
|
||||||
|
class AuthServiceProxyWrapper(object):
|
||||||
|
"""
|
||||||
|
An object that wraps AuthServiceProxy to record specific RPC calls.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, auth_service_proxy_instance, coverage_logfile=None):
|
||||||
|
"""
|
||||||
|
Kwargs:
|
||||||
|
auth_service_proxy_instance (AuthServiceProxy): the instance
|
||||||
|
being wrapped.
|
||||||
|
coverage_logfile (str): if specified, write each service_name
|
||||||
|
out to a file when called.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.auth_service_proxy_instance = auth_service_proxy_instance
|
||||||
|
self.coverage_logfile = coverage_logfile
|
||||||
|
|
||||||
|
def __getattr__(self, *args, **kwargs):
|
||||||
|
return_val = self.auth_service_proxy_instance.__getattr__(
|
||||||
|
*args, **kwargs)
|
||||||
|
|
||||||
|
return AuthServiceProxyWrapper(return_val, self.coverage_logfile)
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Delegates to AuthServiceProxy, then writes the particular RPC method
|
||||||
|
called to a file.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return_val = self.auth_service_proxy_instance.__call__(*args, **kwargs)
|
||||||
|
rpc_method = self.auth_service_proxy_instance._service_name
|
||||||
|
|
||||||
|
if self.coverage_logfile:
|
||||||
|
with open(self.coverage_logfile, 'a+', encoding='utf8') as f:
|
||||||
|
f.write("%s\n" % rpc_method)
|
||||||
|
|
||||||
|
return return_val
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self):
|
||||||
|
return self.auth_service_proxy_instance.url
|
||||||
|
|
||||||
|
|
||||||
|
def get_filename(dirname, n_node):
|
||||||
|
"""
|
||||||
|
Get a filename unique to the test process ID and node.
|
||||||
|
|
||||||
|
This file will contain a list of RPC commands covered.
|
||||||
|
"""
|
||||||
|
pid = str(os.getpid())
|
||||||
|
return os.path.join(
|
||||||
|
dirname, "coverage.pid%s.node%s.txt" % (pid, str(n_node)))
|
||||||
|
|
||||||
|
|
||||||
|
def write_all_rpc_commands(dirname, node):
|
||||||
|
"""
|
||||||
|
Write out a list of all RPC functions available in `bitcoin-cli` for
|
||||||
|
coverage comparison. This will only happen once per coverage
|
||||||
|
directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dirname (str): temporary test dir
|
||||||
|
node (AuthServiceProxy): client
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool. if the RPC interface file was written.
|
||||||
|
|
||||||
|
"""
|
||||||
|
filename = os.path.join(dirname, REFERENCE_FILENAME)
|
||||||
|
|
||||||
|
if os.path.isfile(filename):
|
||||||
|
return False
|
||||||
|
|
||||||
|
help_output = node.help().split('\n')
|
||||||
|
commands = set()
|
||||||
|
|
||||||
|
for line in help_output:
|
||||||
|
line = line.strip()
|
||||||
|
|
||||||
|
# Ignore blanks and headers
|
||||||
|
if line and not line.startswith('='):
|
||||||
|
commands.add("%s\n" % line.split()[0])
|
||||||
|
|
||||||
|
with open(filename, 'w', encoding='utf8') as f:
|
||||||
|
f.writelines(list(commands))
|
||||||
|
|
||||||
|
return True
|
||||||
1904
basicswap/interface/contrib/firo_test_framework/mininode.py
Normal file
943
basicswap/interface/contrib/firo_test_framework/script.py
Normal file
@@ -0,0 +1,943 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright (c) 2015-2016 The Bitcoin Core developers
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
|
#
|
||||||
|
# script.py
|
||||||
|
#
|
||||||
|
# This file is modified from python-bitcoinlib.
|
||||||
|
#
|
||||||
|
|
||||||
|
"""Scripts
|
||||||
|
|
||||||
|
Functionality to build scripts, as well as SignatureHash().
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from .mininode import CTransaction, CTxOut, sha256, hash256, uint256_from_str, ser_uint256, ser_string
|
||||||
|
from binascii import hexlify
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
import sys
|
||||||
|
bchr = chr
|
||||||
|
bord = ord
|
||||||
|
if sys.version > '3':
|
||||||
|
long = int
|
||||||
|
bchr = lambda x: bytes([x])
|
||||||
|
bord = lambda x: x
|
||||||
|
|
||||||
|
import struct
|
||||||
|
|
||||||
|
from .bignum import bn2vch
|
||||||
|
|
||||||
|
MAX_SCRIPT_SIZE = 10000
|
||||||
|
MAX_SCRIPT_ELEMENT_SIZE = 520
|
||||||
|
MAX_SCRIPT_OPCODES = 201
|
||||||
|
|
||||||
|
OPCODE_NAMES = {}
|
||||||
|
|
||||||
|
_opcode_instances = []
|
||||||
|
class CScriptOp(int):
|
||||||
|
"""A single script opcode"""
|
||||||
|
__slots__ = []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def encode_op_pushdata(d):
|
||||||
|
"""Encode a PUSHDATA op, returning bytes"""
|
||||||
|
if len(d) < 0x4c:
|
||||||
|
return b'' + bchr(len(d)) + d # OP_PUSHDATA
|
||||||
|
elif len(d) <= 0xff:
|
||||||
|
return b'\x4c' + bchr(len(d)) + d # OP_PUSHDATA1
|
||||||
|
elif len(d) <= 0xffff:
|
||||||
|
return b'\x4d' + struct.pack(b'<H', len(d)) + d # OP_PUSHDATA2
|
||||||
|
elif len(d) <= 0xffffffff:
|
||||||
|
return b'\x4e' + struct.pack(b'<I', len(d)) + d # OP_PUSHDATA4
|
||||||
|
else:
|
||||||
|
raise ValueError("Data too long to encode in a PUSHDATA op")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def encode_op_n(n):
|
||||||
|
"""Encode a small integer op, returning an opcode"""
|
||||||
|
if not (0 <= n <= 16):
|
||||||
|
raise ValueError('Integer must be in range 0 <= n <= 16, got %d' % n)
|
||||||
|
|
||||||
|
if n == 0:
|
||||||
|
return OP_0
|
||||||
|
else:
|
||||||
|
return CScriptOp(OP_1 + n-1)
|
||||||
|
|
||||||
|
def decode_op_n(self):
|
||||||
|
"""Decode a small integer opcode, returning an integer"""
|
||||||
|
if self == OP_0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if not (self == OP_0 or OP_1 <= self <= OP_16):
|
||||||
|
raise ValueError('op %r is not an OP_N' % self)
|
||||||
|
|
||||||
|
return int(self - OP_1+1)
|
||||||
|
|
||||||
|
def is_small_int(self):
|
||||||
|
"""Return true if the op pushes a small integer to the stack"""
|
||||||
|
if 0x51 <= self <= 0x60 or self == 0:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return repr(self)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if self in OPCODE_NAMES:
|
||||||
|
return OPCODE_NAMES[self]
|
||||||
|
else:
|
||||||
|
return 'CScriptOp(0x%x)' % self
|
||||||
|
|
||||||
|
def __new__(cls, n):
|
||||||
|
try:
|
||||||
|
return _opcode_instances[n]
|
||||||
|
except IndexError:
|
||||||
|
assert len(_opcode_instances) == n
|
||||||
|
_opcode_instances.append(super(CScriptOp, cls).__new__(cls, n))
|
||||||
|
return _opcode_instances[n]
|
||||||
|
|
||||||
|
# Populate opcode instance table
|
||||||
|
for n in range(0xff+1):
|
||||||
|
CScriptOp(n)
|
||||||
|
|
||||||
|
|
||||||
|
# push value
|
||||||
|
OP_0 = CScriptOp(0x00)
|
||||||
|
OP_FALSE = OP_0
|
||||||
|
OP_PUSHDATA1 = CScriptOp(0x4c)
|
||||||
|
OP_PUSHDATA2 = CScriptOp(0x4d)
|
||||||
|
OP_PUSHDATA4 = CScriptOp(0x4e)
|
||||||
|
OP_1NEGATE = CScriptOp(0x4f)
|
||||||
|
OP_RESERVED = CScriptOp(0x50)
|
||||||
|
OP_1 = CScriptOp(0x51)
|
||||||
|
OP_TRUE=OP_1
|
||||||
|
OP_2 = CScriptOp(0x52)
|
||||||
|
OP_3 = CScriptOp(0x53)
|
||||||
|
OP_4 = CScriptOp(0x54)
|
||||||
|
OP_5 = CScriptOp(0x55)
|
||||||
|
OP_6 = CScriptOp(0x56)
|
||||||
|
OP_7 = CScriptOp(0x57)
|
||||||
|
OP_8 = CScriptOp(0x58)
|
||||||
|
OP_9 = CScriptOp(0x59)
|
||||||
|
OP_10 = CScriptOp(0x5a)
|
||||||
|
OP_11 = CScriptOp(0x5b)
|
||||||
|
OP_12 = CScriptOp(0x5c)
|
||||||
|
OP_13 = CScriptOp(0x5d)
|
||||||
|
OP_14 = CScriptOp(0x5e)
|
||||||
|
OP_15 = CScriptOp(0x5f)
|
||||||
|
OP_16 = CScriptOp(0x60)
|
||||||
|
|
||||||
|
# control
|
||||||
|
OP_NOP = CScriptOp(0x61)
|
||||||
|
OP_VER = CScriptOp(0x62)
|
||||||
|
OP_IF = CScriptOp(0x63)
|
||||||
|
OP_NOTIF = CScriptOp(0x64)
|
||||||
|
OP_VERIF = CScriptOp(0x65)
|
||||||
|
OP_VERNOTIF = CScriptOp(0x66)
|
||||||
|
OP_ELSE = CScriptOp(0x67)
|
||||||
|
OP_ENDIF = CScriptOp(0x68)
|
||||||
|
OP_VERIFY = CScriptOp(0x69)
|
||||||
|
OP_RETURN = CScriptOp(0x6a)
|
||||||
|
|
||||||
|
# stack ops
|
||||||
|
OP_TOALTSTACK = CScriptOp(0x6b)
|
||||||
|
OP_FROMALTSTACK = CScriptOp(0x6c)
|
||||||
|
OP_2DROP = CScriptOp(0x6d)
|
||||||
|
OP_2DUP = CScriptOp(0x6e)
|
||||||
|
OP_3DUP = CScriptOp(0x6f)
|
||||||
|
OP_2OVER = CScriptOp(0x70)
|
||||||
|
OP_2ROT = CScriptOp(0x71)
|
||||||
|
OP_2SWAP = CScriptOp(0x72)
|
||||||
|
OP_IFDUP = CScriptOp(0x73)
|
||||||
|
OP_DEPTH = CScriptOp(0x74)
|
||||||
|
OP_DROP = CScriptOp(0x75)
|
||||||
|
OP_DUP = CScriptOp(0x76)
|
||||||
|
OP_NIP = CScriptOp(0x77)
|
||||||
|
OP_OVER = CScriptOp(0x78)
|
||||||
|
OP_PICK = CScriptOp(0x79)
|
||||||
|
OP_ROLL = CScriptOp(0x7a)
|
||||||
|
OP_ROT = CScriptOp(0x7b)
|
||||||
|
OP_SWAP = CScriptOp(0x7c)
|
||||||
|
OP_TUCK = CScriptOp(0x7d)
|
||||||
|
|
||||||
|
# splice ops
|
||||||
|
OP_CAT = CScriptOp(0x7e)
|
||||||
|
OP_SUBSTR = CScriptOp(0x7f)
|
||||||
|
OP_LEFT = CScriptOp(0x80)
|
||||||
|
OP_RIGHT = CScriptOp(0x81)
|
||||||
|
OP_SIZE = CScriptOp(0x82)
|
||||||
|
|
||||||
|
# bit logic
|
||||||
|
OP_INVERT = CScriptOp(0x83)
|
||||||
|
OP_AND = CScriptOp(0x84)
|
||||||
|
OP_OR = CScriptOp(0x85)
|
||||||
|
OP_XOR = CScriptOp(0x86)
|
||||||
|
OP_EQUAL = CScriptOp(0x87)
|
||||||
|
OP_EQUALVERIFY = CScriptOp(0x88)
|
||||||
|
OP_RESERVED1 = CScriptOp(0x89)
|
||||||
|
OP_RESERVED2 = CScriptOp(0x8a)
|
||||||
|
|
||||||
|
# numeric
|
||||||
|
OP_1ADD = CScriptOp(0x8b)
|
||||||
|
OP_1SUB = CScriptOp(0x8c)
|
||||||
|
OP_2MUL = CScriptOp(0x8d)
|
||||||
|
OP_2DIV = CScriptOp(0x8e)
|
||||||
|
OP_NEGATE = CScriptOp(0x8f)
|
||||||
|
OP_ABS = CScriptOp(0x90)
|
||||||
|
OP_NOT = CScriptOp(0x91)
|
||||||
|
OP_0NOTEQUAL = CScriptOp(0x92)
|
||||||
|
|
||||||
|
OP_ADD = CScriptOp(0x93)
|
||||||
|
OP_SUB = CScriptOp(0x94)
|
||||||
|
OP_MUL = CScriptOp(0x95)
|
||||||
|
OP_DIV = CScriptOp(0x96)
|
||||||
|
OP_MOD = CScriptOp(0x97)
|
||||||
|
OP_LSHIFT = CScriptOp(0x98)
|
||||||
|
OP_RSHIFT = CScriptOp(0x99)
|
||||||
|
|
||||||
|
OP_BOOLAND = CScriptOp(0x9a)
|
||||||
|
OP_BOOLOR = CScriptOp(0x9b)
|
||||||
|
OP_NUMEQUAL = CScriptOp(0x9c)
|
||||||
|
OP_NUMEQUALVERIFY = CScriptOp(0x9d)
|
||||||
|
OP_NUMNOTEQUAL = CScriptOp(0x9e)
|
||||||
|
OP_LESSTHAN = CScriptOp(0x9f)
|
||||||
|
OP_GREATERTHAN = CScriptOp(0xa0)
|
||||||
|
OP_LESSTHANOREQUAL = CScriptOp(0xa1)
|
||||||
|
OP_GREATERTHANOREQUAL = CScriptOp(0xa2)
|
||||||
|
OP_MIN = CScriptOp(0xa3)
|
||||||
|
OP_MAX = CScriptOp(0xa4)
|
||||||
|
|
||||||
|
OP_WITHIN = CScriptOp(0xa5)
|
||||||
|
|
||||||
|
# crypto
|
||||||
|
OP_RIPEMD160 = CScriptOp(0xa6)
|
||||||
|
OP_SHA1 = CScriptOp(0xa7)
|
||||||
|
OP_SHA256 = CScriptOp(0xa8)
|
||||||
|
OP_HASH160 = CScriptOp(0xa9)
|
||||||
|
OP_HASH256 = CScriptOp(0xaa)
|
||||||
|
OP_CODESEPARATOR = CScriptOp(0xab)
|
||||||
|
OP_CHECKSIG = CScriptOp(0xac)
|
||||||
|
OP_CHECKSIGVERIFY = CScriptOp(0xad)
|
||||||
|
OP_CHECKMULTISIG = CScriptOp(0xae)
|
||||||
|
OP_CHECKMULTISIGVERIFY = CScriptOp(0xaf)
|
||||||
|
|
||||||
|
# expansion
|
||||||
|
OP_NOP1 = CScriptOp(0xb0)
|
||||||
|
OP_CHECKLOCKTIMEVERIFY = CScriptOp(0xb1)
|
||||||
|
OP_CHECKSEQUENCEVERIFY = CScriptOp(0xb2)
|
||||||
|
OP_NOP4 = CScriptOp(0xb3)
|
||||||
|
OP_NOP5 = CScriptOp(0xb4)
|
||||||
|
OP_NOP6 = CScriptOp(0xb5)
|
||||||
|
OP_NOP7 = CScriptOp(0xb6)
|
||||||
|
OP_NOP8 = CScriptOp(0xb7)
|
||||||
|
OP_NOP9 = CScriptOp(0xb8)
|
||||||
|
OP_NOP10 = CScriptOp(0xb9)
|
||||||
|
|
||||||
|
# template matching params
|
||||||
|
OP_SMALLINTEGER = CScriptOp(0xfa)
|
||||||
|
OP_PUBKEYS = CScriptOp(0xfb)
|
||||||
|
OP_PUBKEYHASH = CScriptOp(0xfd)
|
||||||
|
OP_PUBKEY = CScriptOp(0xfe)
|
||||||
|
|
||||||
|
OP_INVALIDOPCODE = CScriptOp(0xff)
|
||||||
|
|
||||||
|
VALID_OPCODES = {
|
||||||
|
OP_1NEGATE,
|
||||||
|
OP_RESERVED,
|
||||||
|
OP_1,
|
||||||
|
OP_2,
|
||||||
|
OP_3,
|
||||||
|
OP_4,
|
||||||
|
OP_5,
|
||||||
|
OP_6,
|
||||||
|
OP_7,
|
||||||
|
OP_8,
|
||||||
|
OP_9,
|
||||||
|
OP_10,
|
||||||
|
OP_11,
|
||||||
|
OP_12,
|
||||||
|
OP_13,
|
||||||
|
OP_14,
|
||||||
|
OP_15,
|
||||||
|
OP_16,
|
||||||
|
|
||||||
|
OP_NOP,
|
||||||
|
OP_VER,
|
||||||
|
OP_IF,
|
||||||
|
OP_NOTIF,
|
||||||
|
OP_VERIF,
|
||||||
|
OP_VERNOTIF,
|
||||||
|
OP_ELSE,
|
||||||
|
OP_ENDIF,
|
||||||
|
OP_VERIFY,
|
||||||
|
OP_RETURN,
|
||||||
|
|
||||||
|
OP_TOALTSTACK,
|
||||||
|
OP_FROMALTSTACK,
|
||||||
|
OP_2DROP,
|
||||||
|
OP_2DUP,
|
||||||
|
OP_3DUP,
|
||||||
|
OP_2OVER,
|
||||||
|
OP_2ROT,
|
||||||
|
OP_2SWAP,
|
||||||
|
OP_IFDUP,
|
||||||
|
OP_DEPTH,
|
||||||
|
OP_DROP,
|
||||||
|
OP_DUP,
|
||||||
|
OP_NIP,
|
||||||
|
OP_OVER,
|
||||||
|
OP_PICK,
|
||||||
|
OP_ROLL,
|
||||||
|
OP_ROT,
|
||||||
|
OP_SWAP,
|
||||||
|
OP_TUCK,
|
||||||
|
|
||||||
|
OP_CAT,
|
||||||
|
OP_SUBSTR,
|
||||||
|
OP_LEFT,
|
||||||
|
OP_RIGHT,
|
||||||
|
OP_SIZE,
|
||||||
|
|
||||||
|
OP_INVERT,
|
||||||
|
OP_AND,
|
||||||
|
OP_OR,
|
||||||
|
OP_XOR,
|
||||||
|
OP_EQUAL,
|
||||||
|
OP_EQUALVERIFY,
|
||||||
|
OP_RESERVED1,
|
||||||
|
OP_RESERVED2,
|
||||||
|
|
||||||
|
OP_1ADD,
|
||||||
|
OP_1SUB,
|
||||||
|
OP_2MUL,
|
||||||
|
OP_2DIV,
|
||||||
|
OP_NEGATE,
|
||||||
|
OP_ABS,
|
||||||
|
OP_NOT,
|
||||||
|
OP_0NOTEQUAL,
|
||||||
|
|
||||||
|
OP_ADD,
|
||||||
|
OP_SUB,
|
||||||
|
OP_MUL,
|
||||||
|
OP_DIV,
|
||||||
|
OP_MOD,
|
||||||
|
OP_LSHIFT,
|
||||||
|
OP_RSHIFT,
|
||||||
|
|
||||||
|
OP_BOOLAND,
|
||||||
|
OP_BOOLOR,
|
||||||
|
OP_NUMEQUAL,
|
||||||
|
OP_NUMEQUALVERIFY,
|
||||||
|
OP_NUMNOTEQUAL,
|
||||||
|
OP_LESSTHAN,
|
||||||
|
OP_GREATERTHAN,
|
||||||
|
OP_LESSTHANOREQUAL,
|
||||||
|
OP_GREATERTHANOREQUAL,
|
||||||
|
OP_MIN,
|
||||||
|
OP_MAX,
|
||||||
|
|
||||||
|
OP_WITHIN,
|
||||||
|
|
||||||
|
OP_RIPEMD160,
|
||||||
|
OP_SHA1,
|
||||||
|
OP_SHA256,
|
||||||
|
OP_HASH160,
|
||||||
|
OP_HASH256,
|
||||||
|
OP_CODESEPARATOR,
|
||||||
|
OP_CHECKSIG,
|
||||||
|
OP_CHECKSIGVERIFY,
|
||||||
|
OP_CHECKMULTISIG,
|
||||||
|
OP_CHECKMULTISIGVERIFY,
|
||||||
|
|
||||||
|
OP_NOP1,
|
||||||
|
OP_CHECKLOCKTIMEVERIFY,
|
||||||
|
OP_CHECKSEQUENCEVERIFY,
|
||||||
|
OP_NOP4,
|
||||||
|
OP_NOP5,
|
||||||
|
OP_NOP6,
|
||||||
|
OP_NOP7,
|
||||||
|
OP_NOP8,
|
||||||
|
OP_NOP9,
|
||||||
|
OP_NOP10,
|
||||||
|
|
||||||
|
OP_SMALLINTEGER,
|
||||||
|
OP_PUBKEYS,
|
||||||
|
OP_PUBKEYHASH,
|
||||||
|
OP_PUBKEY,
|
||||||
|
}
|
||||||
|
|
||||||
|
OPCODE_NAMES.update({
|
||||||
|
OP_0 : 'OP_0',
|
||||||
|
OP_PUSHDATA1 : 'OP_PUSHDATA1',
|
||||||
|
OP_PUSHDATA2 : 'OP_PUSHDATA2',
|
||||||
|
OP_PUSHDATA4 : 'OP_PUSHDATA4',
|
||||||
|
OP_1NEGATE : 'OP_1NEGATE',
|
||||||
|
OP_RESERVED : 'OP_RESERVED',
|
||||||
|
OP_1 : 'OP_1',
|
||||||
|
OP_2 : 'OP_2',
|
||||||
|
OP_3 : 'OP_3',
|
||||||
|
OP_4 : 'OP_4',
|
||||||
|
OP_5 : 'OP_5',
|
||||||
|
OP_6 : 'OP_6',
|
||||||
|
OP_7 : 'OP_7',
|
||||||
|
OP_8 : 'OP_8',
|
||||||
|
OP_9 : 'OP_9',
|
||||||
|
OP_10 : 'OP_10',
|
||||||
|
OP_11 : 'OP_11',
|
||||||
|
OP_12 : 'OP_12',
|
||||||
|
OP_13 : 'OP_13',
|
||||||
|
OP_14 : 'OP_14',
|
||||||
|
OP_15 : 'OP_15',
|
||||||
|
OP_16 : 'OP_16',
|
||||||
|
OP_NOP : 'OP_NOP',
|
||||||
|
OP_VER : 'OP_VER',
|
||||||
|
OP_IF : 'OP_IF',
|
||||||
|
OP_NOTIF : 'OP_NOTIF',
|
||||||
|
OP_VERIF : 'OP_VERIF',
|
||||||
|
OP_VERNOTIF : 'OP_VERNOTIF',
|
||||||
|
OP_ELSE : 'OP_ELSE',
|
||||||
|
OP_ENDIF : 'OP_ENDIF',
|
||||||
|
OP_VERIFY : 'OP_VERIFY',
|
||||||
|
OP_RETURN : 'OP_RETURN',
|
||||||
|
OP_TOALTSTACK : 'OP_TOALTSTACK',
|
||||||
|
OP_FROMALTSTACK : 'OP_FROMALTSTACK',
|
||||||
|
OP_2DROP : 'OP_2DROP',
|
||||||
|
OP_2DUP : 'OP_2DUP',
|
||||||
|
OP_3DUP : 'OP_3DUP',
|
||||||
|
OP_2OVER : 'OP_2OVER',
|
||||||
|
OP_2ROT : 'OP_2ROT',
|
||||||
|
OP_2SWAP : 'OP_2SWAP',
|
||||||
|
OP_IFDUP : 'OP_IFDUP',
|
||||||
|
OP_DEPTH : 'OP_DEPTH',
|
||||||
|
OP_DROP : 'OP_DROP',
|
||||||
|
OP_DUP : 'OP_DUP',
|
||||||
|
OP_NIP : 'OP_NIP',
|
||||||
|
OP_OVER : 'OP_OVER',
|
||||||
|
OP_PICK : 'OP_PICK',
|
||||||
|
OP_ROLL : 'OP_ROLL',
|
||||||
|
OP_ROT : 'OP_ROT',
|
||||||
|
OP_SWAP : 'OP_SWAP',
|
||||||
|
OP_TUCK : 'OP_TUCK',
|
||||||
|
OP_CAT : 'OP_CAT',
|
||||||
|
OP_SUBSTR : 'OP_SUBSTR',
|
||||||
|
OP_LEFT : 'OP_LEFT',
|
||||||
|
OP_RIGHT : 'OP_RIGHT',
|
||||||
|
OP_SIZE : 'OP_SIZE',
|
||||||
|
OP_INVERT : 'OP_INVERT',
|
||||||
|
OP_AND : 'OP_AND',
|
||||||
|
OP_OR : 'OP_OR',
|
||||||
|
OP_XOR : 'OP_XOR',
|
||||||
|
OP_EQUAL : 'OP_EQUAL',
|
||||||
|
OP_EQUALVERIFY : 'OP_EQUALVERIFY',
|
||||||
|
OP_RESERVED1 : 'OP_RESERVED1',
|
||||||
|
OP_RESERVED2 : 'OP_RESERVED2',
|
||||||
|
OP_1ADD : 'OP_1ADD',
|
||||||
|
OP_1SUB : 'OP_1SUB',
|
||||||
|
OP_2MUL : 'OP_2MUL',
|
||||||
|
OP_2DIV : 'OP_2DIV',
|
||||||
|
OP_NEGATE : 'OP_NEGATE',
|
||||||
|
OP_ABS : 'OP_ABS',
|
||||||
|
OP_NOT : 'OP_NOT',
|
||||||
|
OP_0NOTEQUAL : 'OP_0NOTEQUAL',
|
||||||
|
OP_ADD : 'OP_ADD',
|
||||||
|
OP_SUB : 'OP_SUB',
|
||||||
|
OP_MUL : 'OP_MUL',
|
||||||
|
OP_DIV : 'OP_DIV',
|
||||||
|
OP_MOD : 'OP_MOD',
|
||||||
|
OP_LSHIFT : 'OP_LSHIFT',
|
||||||
|
OP_RSHIFT : 'OP_RSHIFT',
|
||||||
|
OP_BOOLAND : 'OP_BOOLAND',
|
||||||
|
OP_BOOLOR : 'OP_BOOLOR',
|
||||||
|
OP_NUMEQUAL : 'OP_NUMEQUAL',
|
||||||
|
OP_NUMEQUALVERIFY : 'OP_NUMEQUALVERIFY',
|
||||||
|
OP_NUMNOTEQUAL : 'OP_NUMNOTEQUAL',
|
||||||
|
OP_LESSTHAN : 'OP_LESSTHAN',
|
||||||
|
OP_GREATERTHAN : 'OP_GREATERTHAN',
|
||||||
|
OP_LESSTHANOREQUAL : 'OP_LESSTHANOREQUAL',
|
||||||
|
OP_GREATERTHANOREQUAL : 'OP_GREATERTHANOREQUAL',
|
||||||
|
OP_MIN : 'OP_MIN',
|
||||||
|
OP_MAX : 'OP_MAX',
|
||||||
|
OP_WITHIN : 'OP_WITHIN',
|
||||||
|
OP_RIPEMD160 : 'OP_RIPEMD160',
|
||||||
|
OP_SHA1 : 'OP_SHA1',
|
||||||
|
OP_SHA256 : 'OP_SHA256',
|
||||||
|
OP_HASH160 : 'OP_HASH160',
|
||||||
|
OP_HASH256 : 'OP_HASH256',
|
||||||
|
OP_CODESEPARATOR : 'OP_CODESEPARATOR',
|
||||||
|
OP_CHECKSIG : 'OP_CHECKSIG',
|
||||||
|
OP_CHECKSIGVERIFY : 'OP_CHECKSIGVERIFY',
|
||||||
|
OP_CHECKMULTISIG : 'OP_CHECKMULTISIG',
|
||||||
|
OP_CHECKMULTISIGVERIFY : 'OP_CHECKMULTISIGVERIFY',
|
||||||
|
OP_NOP1 : 'OP_NOP1',
|
||||||
|
OP_CHECKLOCKTIMEVERIFY : 'OP_CHECKLOCKTIMEVERIFY',
|
||||||
|
OP_CHECKSEQUENCEVERIFY : 'OP_CHECKSEQUENCEVERIFY',
|
||||||
|
OP_NOP4 : 'OP_NOP4',
|
||||||
|
OP_NOP5 : 'OP_NOP5',
|
||||||
|
OP_NOP6 : 'OP_NOP6',
|
||||||
|
OP_NOP7 : 'OP_NOP7',
|
||||||
|
OP_NOP8 : 'OP_NOP8',
|
||||||
|
OP_NOP9 : 'OP_NOP9',
|
||||||
|
OP_NOP10 : 'OP_NOP10',
|
||||||
|
OP_SMALLINTEGER : 'OP_SMALLINTEGER',
|
||||||
|
OP_PUBKEYS : 'OP_PUBKEYS',
|
||||||
|
OP_PUBKEYHASH : 'OP_PUBKEYHASH',
|
||||||
|
OP_PUBKEY : 'OP_PUBKEY',
|
||||||
|
OP_INVALIDOPCODE : 'OP_INVALIDOPCODE',
|
||||||
|
})
|
||||||
|
|
||||||
|
OPCODES_BY_NAME = {
|
||||||
|
'OP_0' : OP_0,
|
||||||
|
'OP_PUSHDATA1' : OP_PUSHDATA1,
|
||||||
|
'OP_PUSHDATA2' : OP_PUSHDATA2,
|
||||||
|
'OP_PUSHDATA4' : OP_PUSHDATA4,
|
||||||
|
'OP_1NEGATE' : OP_1NEGATE,
|
||||||
|
'OP_RESERVED' : OP_RESERVED,
|
||||||
|
'OP_1' : OP_1,
|
||||||
|
'OP_2' : OP_2,
|
||||||
|
'OP_3' : OP_3,
|
||||||
|
'OP_4' : OP_4,
|
||||||
|
'OP_5' : OP_5,
|
||||||
|
'OP_6' : OP_6,
|
||||||
|
'OP_7' : OP_7,
|
||||||
|
'OP_8' : OP_8,
|
||||||
|
'OP_9' : OP_9,
|
||||||
|
'OP_10' : OP_10,
|
||||||
|
'OP_11' : OP_11,
|
||||||
|
'OP_12' : OP_12,
|
||||||
|
'OP_13' : OP_13,
|
||||||
|
'OP_14' : OP_14,
|
||||||
|
'OP_15' : OP_15,
|
||||||
|
'OP_16' : OP_16,
|
||||||
|
'OP_NOP' : OP_NOP,
|
||||||
|
'OP_VER' : OP_VER,
|
||||||
|
'OP_IF' : OP_IF,
|
||||||
|
'OP_NOTIF' : OP_NOTIF,
|
||||||
|
'OP_VERIF' : OP_VERIF,
|
||||||
|
'OP_VERNOTIF' : OP_VERNOTIF,
|
||||||
|
'OP_ELSE' : OP_ELSE,
|
||||||
|
'OP_ENDIF' : OP_ENDIF,
|
||||||
|
'OP_VERIFY' : OP_VERIFY,
|
||||||
|
'OP_RETURN' : OP_RETURN,
|
||||||
|
'OP_TOALTSTACK' : OP_TOALTSTACK,
|
||||||
|
'OP_FROMALTSTACK' : OP_FROMALTSTACK,
|
||||||
|
'OP_2DROP' : OP_2DROP,
|
||||||
|
'OP_2DUP' : OP_2DUP,
|
||||||
|
'OP_3DUP' : OP_3DUP,
|
||||||
|
'OP_2OVER' : OP_2OVER,
|
||||||
|
'OP_2ROT' : OP_2ROT,
|
||||||
|
'OP_2SWAP' : OP_2SWAP,
|
||||||
|
'OP_IFDUP' : OP_IFDUP,
|
||||||
|
'OP_DEPTH' : OP_DEPTH,
|
||||||
|
'OP_DROP' : OP_DROP,
|
||||||
|
'OP_DUP' : OP_DUP,
|
||||||
|
'OP_NIP' : OP_NIP,
|
||||||
|
'OP_OVER' : OP_OVER,
|
||||||
|
'OP_PICK' : OP_PICK,
|
||||||
|
'OP_ROLL' : OP_ROLL,
|
||||||
|
'OP_ROT' : OP_ROT,
|
||||||
|
'OP_SWAP' : OP_SWAP,
|
||||||
|
'OP_TUCK' : OP_TUCK,
|
||||||
|
'OP_CAT' : OP_CAT,
|
||||||
|
'OP_SUBSTR' : OP_SUBSTR,
|
||||||
|
'OP_LEFT' : OP_LEFT,
|
||||||
|
'OP_RIGHT' : OP_RIGHT,
|
||||||
|
'OP_SIZE' : OP_SIZE,
|
||||||
|
'OP_INVERT' : OP_INVERT,
|
||||||
|
'OP_AND' : OP_AND,
|
||||||
|
'OP_OR' : OP_OR,
|
||||||
|
'OP_XOR' : OP_XOR,
|
||||||
|
'OP_EQUAL' : OP_EQUAL,
|
||||||
|
'OP_EQUALVERIFY' : OP_EQUALVERIFY,
|
||||||
|
'OP_RESERVED1' : OP_RESERVED1,
|
||||||
|
'OP_RESERVED2' : OP_RESERVED2,
|
||||||
|
'OP_1ADD' : OP_1ADD,
|
||||||
|
'OP_1SUB' : OP_1SUB,
|
||||||
|
'OP_2MUL' : OP_2MUL,
|
||||||
|
'OP_2DIV' : OP_2DIV,
|
||||||
|
'OP_NEGATE' : OP_NEGATE,
|
||||||
|
'OP_ABS' : OP_ABS,
|
||||||
|
'OP_NOT' : OP_NOT,
|
||||||
|
'OP_0NOTEQUAL' : OP_0NOTEQUAL,
|
||||||
|
'OP_ADD' : OP_ADD,
|
||||||
|
'OP_SUB' : OP_SUB,
|
||||||
|
'OP_MUL' : OP_MUL,
|
||||||
|
'OP_DIV' : OP_DIV,
|
||||||
|
'OP_MOD' : OP_MOD,
|
||||||
|
'OP_LSHIFT' : OP_LSHIFT,
|
||||||
|
'OP_RSHIFT' : OP_RSHIFT,
|
||||||
|
'OP_BOOLAND' : OP_BOOLAND,
|
||||||
|
'OP_BOOLOR' : OP_BOOLOR,
|
||||||
|
'OP_NUMEQUAL' : OP_NUMEQUAL,
|
||||||
|
'OP_NUMEQUALVERIFY' : OP_NUMEQUALVERIFY,
|
||||||
|
'OP_NUMNOTEQUAL' : OP_NUMNOTEQUAL,
|
||||||
|
'OP_LESSTHAN' : OP_LESSTHAN,
|
||||||
|
'OP_GREATERTHAN' : OP_GREATERTHAN,
|
||||||
|
'OP_LESSTHANOREQUAL' : OP_LESSTHANOREQUAL,
|
||||||
|
'OP_GREATERTHANOREQUAL' : OP_GREATERTHANOREQUAL,
|
||||||
|
'OP_MIN' : OP_MIN,
|
||||||
|
'OP_MAX' : OP_MAX,
|
||||||
|
'OP_WITHIN' : OP_WITHIN,
|
||||||
|
'OP_RIPEMD160' : OP_RIPEMD160,
|
||||||
|
'OP_SHA1' : OP_SHA1,
|
||||||
|
'OP_SHA256' : OP_SHA256,
|
||||||
|
'OP_HASH160' : OP_HASH160,
|
||||||
|
'OP_HASH256' : OP_HASH256,
|
||||||
|
'OP_CODESEPARATOR' : OP_CODESEPARATOR,
|
||||||
|
'OP_CHECKSIG' : OP_CHECKSIG,
|
||||||
|
'OP_CHECKSIGVERIFY' : OP_CHECKSIGVERIFY,
|
||||||
|
'OP_CHECKMULTISIG' : OP_CHECKMULTISIG,
|
||||||
|
'OP_CHECKMULTISIGVERIFY' : OP_CHECKMULTISIGVERIFY,
|
||||||
|
'OP_NOP1' : OP_NOP1,
|
||||||
|
'OP_CHECKLOCKTIMEVERIFY' : OP_CHECKLOCKTIMEVERIFY,
|
||||||
|
'OP_CHECKSEQUENCEVERIFY' : OP_CHECKSEQUENCEVERIFY,
|
||||||
|
'OP_NOP4' : OP_NOP4,
|
||||||
|
'OP_NOP5' : OP_NOP5,
|
||||||
|
'OP_NOP6' : OP_NOP6,
|
||||||
|
'OP_NOP7' : OP_NOP7,
|
||||||
|
'OP_NOP8' : OP_NOP8,
|
||||||
|
'OP_NOP9' : OP_NOP9,
|
||||||
|
'OP_NOP10' : OP_NOP10,
|
||||||
|
'OP_SMALLINTEGER' : OP_SMALLINTEGER,
|
||||||
|
'OP_PUBKEYS' : OP_PUBKEYS,
|
||||||
|
'OP_PUBKEYHASH' : OP_PUBKEYHASH,
|
||||||
|
'OP_PUBKEY' : OP_PUBKEY,
|
||||||
|
}
|
||||||
|
|
||||||
|
class CScriptInvalidError(Exception):
|
||||||
|
"""Base class for CScript exceptions"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class CScriptTruncatedPushDataError(CScriptInvalidError):
|
||||||
|
"""Invalid pushdata due to truncation"""
|
||||||
|
def __init__(self, msg, data):
|
||||||
|
self.data = data
|
||||||
|
super(CScriptTruncatedPushDataError, self).__init__(msg)
|
||||||
|
|
||||||
|
# This is used, eg, for blockchain heights in coinbase scripts (bip34)
|
||||||
|
class CScriptNum(object):
|
||||||
|
def __init__(self, d=0):
|
||||||
|
self.value = d
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def encode(obj):
|
||||||
|
r = bytearray(0)
|
||||||
|
if obj.value == 0:
|
||||||
|
return bytes(r)
|
||||||
|
neg = obj.value < 0
|
||||||
|
absvalue = -obj.value if neg else obj.value
|
||||||
|
while (absvalue):
|
||||||
|
r.append(absvalue & 0xff)
|
||||||
|
absvalue >>= 8
|
||||||
|
if r[-1] & 0x80:
|
||||||
|
r.append(0x80 if neg else 0)
|
||||||
|
elif neg:
|
||||||
|
r[-1] |= 0x80
|
||||||
|
return bytes(bchr(len(r)) + r)
|
||||||
|
|
||||||
|
|
||||||
|
class CScript(bytes):
|
||||||
|
"""Serialized script
|
||||||
|
|
||||||
|
A bytes subclass, so you can use this directly whenever bytes are accepted.
|
||||||
|
Note that this means that indexing does *not* work - you'll get an index by
|
||||||
|
byte rather than opcode. This format was chosen for efficiency so that the
|
||||||
|
general case would not require creating a lot of little CScriptOP objects.
|
||||||
|
|
||||||
|
iter(script) however does iterate by opcode.
|
||||||
|
"""
|
||||||
|
@classmethod
|
||||||
|
def __coerce_instance(cls, other):
|
||||||
|
# Coerce other into bytes
|
||||||
|
if isinstance(other, CScriptOp):
|
||||||
|
other = bchr(other)
|
||||||
|
elif isinstance(other, CScriptNum):
|
||||||
|
if (other.value == 0):
|
||||||
|
other = bchr(CScriptOp(OP_0))
|
||||||
|
else:
|
||||||
|
other = CScriptNum.encode(other)
|
||||||
|
elif isinstance(other, int):
|
||||||
|
if 0 <= other <= 16:
|
||||||
|
other = bytes(bchr(CScriptOp.encode_op_n(other)))
|
||||||
|
elif other == -1:
|
||||||
|
other = bytes(bchr(OP_1NEGATE))
|
||||||
|
else:
|
||||||
|
other = CScriptOp.encode_op_pushdata(bn2vch(other))
|
||||||
|
elif isinstance(other, (bytes, bytearray)):
|
||||||
|
other = CScriptOp.encode_op_pushdata(other)
|
||||||
|
return other
|
||||||
|
|
||||||
|
def __add__(self, other):
|
||||||
|
# Do the coercion outside of the try block so that errors in it are
|
||||||
|
# noticed.
|
||||||
|
other = self.__coerce_instance(other)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# bytes.__add__ always returns bytes instances unfortunately
|
||||||
|
return CScript(super(CScript, self).__add__(other))
|
||||||
|
except TypeError:
|
||||||
|
raise TypeError('Can not add a %r instance to a CScript' % other.__class__)
|
||||||
|
|
||||||
|
def join(self, iterable):
|
||||||
|
# join makes no sense for a CScript()
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def __new__(cls, value=b''):
|
||||||
|
if isinstance(value, bytes) or isinstance(value, bytearray):
|
||||||
|
return super(CScript, cls).__new__(cls, value)
|
||||||
|
else:
|
||||||
|
def coerce_iterable(iterable):
|
||||||
|
for instance in iterable:
|
||||||
|
yield cls.__coerce_instance(instance)
|
||||||
|
# Annoyingly on both python2 and python3 bytes.join() always
|
||||||
|
# returns a bytes instance even when subclassed.
|
||||||
|
return super(CScript, cls).__new__(cls, b''.join(coerce_iterable(value)))
|
||||||
|
|
||||||
|
def raw_iter(self):
|
||||||
|
"""Raw iteration
|
||||||
|
|
||||||
|
Yields tuples of (opcode, data, sop_idx) so that the different possible
|
||||||
|
PUSHDATA encodings can be accurately distinguished, as well as
|
||||||
|
determining the exact opcode byte indexes. (sop_idx)
|
||||||
|
"""
|
||||||
|
i = 0
|
||||||
|
while i < len(self):
|
||||||
|
sop_idx = i
|
||||||
|
opcode = bord(self[i])
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if opcode > OP_PUSHDATA4:
|
||||||
|
yield (opcode, None, sop_idx)
|
||||||
|
else:
|
||||||
|
datasize = None
|
||||||
|
pushdata_type = None
|
||||||
|
if opcode < OP_PUSHDATA1:
|
||||||
|
pushdata_type = 'PUSHDATA(%d)' % opcode
|
||||||
|
datasize = opcode
|
||||||
|
|
||||||
|
elif opcode == OP_PUSHDATA1:
|
||||||
|
pushdata_type = 'PUSHDATA1'
|
||||||
|
if i >= len(self):
|
||||||
|
raise CScriptInvalidError('PUSHDATA1: missing data length')
|
||||||
|
datasize = bord(self[i])
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
elif opcode == OP_PUSHDATA2:
|
||||||
|
pushdata_type = 'PUSHDATA2'
|
||||||
|
if i + 1 >= len(self):
|
||||||
|
raise CScriptInvalidError('PUSHDATA2: missing data length')
|
||||||
|
datasize = bord(self[i]) + (bord(self[i+1]) << 8)
|
||||||
|
i += 2
|
||||||
|
|
||||||
|
elif opcode == OP_PUSHDATA4:
|
||||||
|
pushdata_type = 'PUSHDATA4'
|
||||||
|
if i + 3 >= len(self):
|
||||||
|
raise CScriptInvalidError('PUSHDATA4: missing data length')
|
||||||
|
datasize = bord(self[i]) + (bord(self[i+1]) << 8) + (bord(self[i+2]) << 16) + (bord(self[i+3]) << 24)
|
||||||
|
i += 4
|
||||||
|
|
||||||
|
else:
|
||||||
|
assert False # shouldn't happen
|
||||||
|
|
||||||
|
|
||||||
|
data = bytes(self[i:i+datasize])
|
||||||
|
|
||||||
|
# Check for truncation
|
||||||
|
if len(data) < datasize:
|
||||||
|
raise CScriptTruncatedPushDataError('%s: truncated data' % pushdata_type, data)
|
||||||
|
|
||||||
|
i += datasize
|
||||||
|
|
||||||
|
yield (opcode, data, sop_idx)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
"""'Cooked' iteration
|
||||||
|
|
||||||
|
Returns either a CScriptOP instance, an integer, or bytes, as
|
||||||
|
appropriate.
|
||||||
|
|
||||||
|
See raw_iter() if you need to distinguish the different possible
|
||||||
|
PUSHDATA encodings.
|
||||||
|
"""
|
||||||
|
for (opcode, data, sop_idx) in self.raw_iter():
|
||||||
|
if data is not None:
|
||||||
|
yield data
|
||||||
|
else:
|
||||||
|
opcode = CScriptOp(opcode)
|
||||||
|
|
||||||
|
if opcode.is_small_int():
|
||||||
|
yield opcode.decode_op_n()
|
||||||
|
else:
|
||||||
|
yield CScriptOp(opcode)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
# For Python3 compatibility add b before strings so testcases don't
|
||||||
|
# need to change
|
||||||
|
def _repr(o):
|
||||||
|
if isinstance(o, bytes):
|
||||||
|
return b"x('%s')" % hexlify(o).decode('ascii')
|
||||||
|
else:
|
||||||
|
return repr(o)
|
||||||
|
|
||||||
|
ops = []
|
||||||
|
i = iter(self)
|
||||||
|
while True:
|
||||||
|
op = None
|
||||||
|
try:
|
||||||
|
op = _repr(next(i))
|
||||||
|
except CScriptTruncatedPushDataError as err:
|
||||||
|
op = '%s...<ERROR: %s>' % (_repr(err.data), err)
|
||||||
|
break
|
||||||
|
except CScriptInvalidError as err:
|
||||||
|
op = '<ERROR: %s>' % err
|
||||||
|
break
|
||||||
|
except StopIteration:
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
if op is not None:
|
||||||
|
ops.append(op)
|
||||||
|
|
||||||
|
return "CScript([%s])" % ', '.join(ops)
|
||||||
|
|
||||||
|
def GetSigOpCount(self, fAccurate):
|
||||||
|
"""Get the SigOp count.
|
||||||
|
|
||||||
|
fAccurate - Accurately count CHECKMULTISIG, see BIP16 for details.
|
||||||
|
|
||||||
|
Note that this is consensus-critical.
|
||||||
|
"""
|
||||||
|
n = 0
|
||||||
|
lastOpcode = OP_INVALIDOPCODE
|
||||||
|
for (opcode, data, sop_idx) in self.raw_iter():
|
||||||
|
if opcode in (OP_CHECKSIG, OP_CHECKSIGVERIFY):
|
||||||
|
n += 1
|
||||||
|
elif opcode in (OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY):
|
||||||
|
if fAccurate and (OP_1 <= lastOpcode <= OP_16):
|
||||||
|
n += opcode.decode_op_n()
|
||||||
|
else:
|
||||||
|
n += 20
|
||||||
|
lastOpcode = opcode
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
SIGHASH_ALL = 1
|
||||||
|
SIGHASH_NONE = 2
|
||||||
|
SIGHASH_SINGLE = 3
|
||||||
|
SIGHASH_ANYONECANPAY = 0x80
|
||||||
|
|
||||||
|
def FindAndDelete(script, sig):
|
||||||
|
"""Consensus critical, see FindAndDelete() in Satoshi codebase"""
|
||||||
|
r = b''
|
||||||
|
last_sop_idx = sop_idx = 0
|
||||||
|
skip = True
|
||||||
|
for (opcode, data, sop_idx) in script.raw_iter():
|
||||||
|
if not skip:
|
||||||
|
r += script[last_sop_idx:sop_idx]
|
||||||
|
last_sop_idx = sop_idx
|
||||||
|
if script[sop_idx:sop_idx + len(sig)] == sig:
|
||||||
|
skip = True
|
||||||
|
else:
|
||||||
|
skip = False
|
||||||
|
if not skip:
|
||||||
|
r += script[last_sop_idx:]
|
||||||
|
return CScript(r)
|
||||||
|
|
||||||
|
|
||||||
|
def SignatureHash(script, txTo, inIdx, hashtype):
|
||||||
|
"""Consensus-correct SignatureHash
|
||||||
|
|
||||||
|
Returns (hash, err) to precisely match the consensus-critical behavior of
|
||||||
|
the SIGHASH_SINGLE bug. (inIdx is *not* checked for validity)
|
||||||
|
"""
|
||||||
|
HASH_ONE = b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
|
||||||
|
if inIdx >= len(txTo.vin):
|
||||||
|
return (HASH_ONE, "inIdx %d out of range (%d)" % (inIdx, len(txTo.vin)))
|
||||||
|
txtmp = CTransaction(txTo)
|
||||||
|
|
||||||
|
for txin in txtmp.vin:
|
||||||
|
txin.scriptSig = b''
|
||||||
|
txtmp.vin[inIdx].scriptSig = FindAndDelete(script, CScript([OP_CODESEPARATOR]))
|
||||||
|
|
||||||
|
if (hashtype & 0x1f) == SIGHASH_NONE:
|
||||||
|
txtmp.vout = []
|
||||||
|
|
||||||
|
for i in range(len(txtmp.vin)):
|
||||||
|
if i != inIdx:
|
||||||
|
txtmp.vin[i].nSequence = 0
|
||||||
|
|
||||||
|
elif (hashtype & 0x1f) == SIGHASH_SINGLE:
|
||||||
|
outIdx = inIdx
|
||||||
|
if outIdx >= len(txtmp.vout):
|
||||||
|
return (HASH_ONE, "outIdx %d out of range (%d)" % (outIdx, len(txtmp.vout)))
|
||||||
|
|
||||||
|
tmp = txtmp.vout[outIdx]
|
||||||
|
txtmp.vout = []
|
||||||
|
for i in range(outIdx):
|
||||||
|
txtmp.vout.append(CTxOut(-1))
|
||||||
|
txtmp.vout.append(tmp)
|
||||||
|
|
||||||
|
for i in range(len(txtmp.vin)):
|
||||||
|
if i != inIdx:
|
||||||
|
txtmp.vin[i].nSequence = 0
|
||||||
|
|
||||||
|
if hashtype & SIGHASH_ANYONECANPAY:
|
||||||
|
tmp = txtmp.vin[inIdx]
|
||||||
|
txtmp.vin = []
|
||||||
|
txtmp.vin.append(tmp)
|
||||||
|
|
||||||
|
s = txtmp.serialize()
|
||||||
|
s += struct.pack(b"<I", hashtype)
|
||||||
|
|
||||||
|
hash = hash256(s)
|
||||||
|
|
||||||
|
return (hash, None)
|
||||||
|
|
||||||
|
# TODO: Allow cached hashPrevouts/hashSequence/hashOutputs to be provided.
|
||||||
|
# Performance optimization probably not necessary for python tests, however.
|
||||||
|
# Note that this corresponds to sigversion == 1 in EvalScript, which is used
|
||||||
|
# for version 0 witnesses.
|
||||||
|
def SegwitVersion1SignatureHash(script, txTo, inIdx, hashtype, amount):
|
||||||
|
|
||||||
|
hashPrevouts = 0
|
||||||
|
hashSequence = 0
|
||||||
|
hashOutputs = 0
|
||||||
|
|
||||||
|
if not (hashtype & SIGHASH_ANYONECANPAY):
|
||||||
|
serialize_prevouts = bytes()
|
||||||
|
for i in txTo.vin:
|
||||||
|
serialize_prevouts += i.prevout.serialize()
|
||||||
|
hashPrevouts = uint256_from_str(hash256(serialize_prevouts))
|
||||||
|
|
||||||
|
if (not (hashtype & SIGHASH_ANYONECANPAY) and (hashtype & 0x1f) != SIGHASH_SINGLE and (hashtype & 0x1f) != SIGHASH_NONE):
|
||||||
|
serialize_sequence = bytes()
|
||||||
|
for i in txTo.vin:
|
||||||
|
serialize_sequence += struct.pack("<I", i.nSequence)
|
||||||
|
hashSequence = uint256_from_str(hash256(serialize_sequence))
|
||||||
|
|
||||||
|
if ((hashtype & 0x1f) != SIGHASH_SINGLE and (hashtype & 0x1f) != SIGHASH_NONE):
|
||||||
|
serialize_outputs = bytes()
|
||||||
|
for o in txTo.vout:
|
||||||
|
serialize_outputs += o.serialize()
|
||||||
|
hashOutputs = uint256_from_str(hash256(serialize_outputs))
|
||||||
|
elif ((hashtype & 0x1f) == SIGHASH_SINGLE and inIdx < len(txTo.vout)):
|
||||||
|
serialize_outputs = txTo.vout[inIdx].serialize()
|
||||||
|
hashOutputs = uint256_from_str(hash256(serialize_outputs))
|
||||||
|
|
||||||
|
ss = bytes()
|
||||||
|
ss += struct.pack("<i", txTo.nVersion)
|
||||||
|
ss += ser_uint256(hashPrevouts)
|
||||||
|
ss += ser_uint256(hashSequence)
|
||||||
|
ss += txTo.vin[inIdx].prevout.serialize()
|
||||||
|
ss += ser_string(script)
|
||||||
|
ss += struct.pack("<q", amount)
|
||||||
|
ss += struct.pack("<I", txTo.vin[inIdx].nSequence)
|
||||||
|
ss += ser_uint256(hashOutputs)
|
||||||
|
ss += struct.pack("<i", txTo.nLockTime)
|
||||||
|
ss += struct.pack("<I", hashtype)
|
||||||
|
|
||||||
|
return hash256(ss)
|
||||||
64
basicswap/interface/contrib/firo_test_framework/siphash.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright (c) 2016 The Bitcoin Core developers
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
|
#
|
||||||
|
# siphash.py - Specialized SipHash-2-4 implementations
|
||||||
|
#
|
||||||
|
# This implements SipHash-2-4 for 256-bit integers.
|
||||||
|
|
||||||
|
def rotl64(n, b):
|
||||||
|
return n >> (64 - b) | (n & ((1 << (64 - b)) - 1)) << b
|
||||||
|
|
||||||
|
def siphash_round(v0, v1, v2, v3):
|
||||||
|
v0 = (v0 + v1) & ((1 << 64) - 1)
|
||||||
|
v1 = rotl64(v1, 13)
|
||||||
|
v1 ^= v0
|
||||||
|
v0 = rotl64(v0, 32)
|
||||||
|
v2 = (v2 + v3) & ((1 << 64) - 1)
|
||||||
|
v3 = rotl64(v3, 16)
|
||||||
|
v3 ^= v2
|
||||||
|
v0 = (v0 + v3) & ((1 << 64) - 1)
|
||||||
|
v3 = rotl64(v3, 21)
|
||||||
|
v3 ^= v0
|
||||||
|
v2 = (v2 + v1) & ((1 << 64) - 1)
|
||||||
|
v1 = rotl64(v1, 17)
|
||||||
|
v1 ^= v2
|
||||||
|
v2 = rotl64(v2, 32)
|
||||||
|
return (v0, v1, v2, v3)
|
||||||
|
|
||||||
|
def siphash256(k0, k1, h):
|
||||||
|
n0 = h & ((1 << 64) - 1)
|
||||||
|
n1 = (h >> 64) & ((1 << 64) - 1)
|
||||||
|
n2 = (h >> 128) & ((1 << 64) - 1)
|
||||||
|
n3 = (h >> 192) & ((1 << 64) - 1)
|
||||||
|
v0 = 0x736f6d6570736575 ^ k0
|
||||||
|
v1 = 0x646f72616e646f6d ^ k1
|
||||||
|
v2 = 0x6c7967656e657261 ^ k0
|
||||||
|
v3 = 0x7465646279746573 ^ k1 ^ n0
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0 ^= n0
|
||||||
|
v3 ^= n1
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0 ^= n1
|
||||||
|
v3 ^= n2
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0 ^= n2
|
||||||
|
v3 ^= n3
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0 ^= n3
|
||||||
|
v3 ^= 0x2000000000000000
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0 ^= 0x2000000000000000
|
||||||
|
v2 ^= 0xFF
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
return v0 ^ v1 ^ v2 ^ v3
|
||||||
841
basicswap/interface/contrib/firo_test_framework/util.py
Normal file
@@ -0,0 +1,841 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright (c) 2014-2016 The Bitcoin Core developers
|
||||||
|
# Copyright (c) 2014-2017 The Dash Core developers
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Helpful routines for regression testing
|
||||||
|
#
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from binascii import hexlify, unhexlify
|
||||||
|
from base64 import b64encode
|
||||||
|
from decimal import Decimal, ROUND_DOWN
|
||||||
|
import json
|
||||||
|
import http.client
|
||||||
|
import random
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
import errno
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from . import coverage
|
||||||
|
from .authproxy import AuthServiceProxy, JSONRPCException
|
||||||
|
|
||||||
|
COVERAGE_DIR = None
|
||||||
|
|
||||||
|
logger = logging.getLogger("TestFramework.utils")
|
||||||
|
# The maximum number of nodes a single test can spawn
|
||||||
|
MAX_NODES = 15
|
||||||
|
# Don't assign rpc or p2p ports lower than this
|
||||||
|
PORT_MIN = 11000
|
||||||
|
# The number of ports to "reserve" for p2p and rpc, each
|
||||||
|
PORT_RANGE = 5000
|
||||||
|
|
||||||
|
BITCOIND_PROC_WAIT_TIMEOUT = 60
|
||||||
|
|
||||||
|
|
||||||
|
class PortSeed:
|
||||||
|
# Must be initialized with a unique integer for each process
|
||||||
|
n = None
|
||||||
|
|
||||||
|
#Set Mocktime default to OFF.
|
||||||
|
#MOCKTIME is only needed for scripts that use the
|
||||||
|
#cached version of the blockchain. If the cached
|
||||||
|
#version of the blockchain is used without MOCKTIME
|
||||||
|
#then the mempools will not sync due to IBD.
|
||||||
|
MOCKTIME = 0
|
||||||
|
|
||||||
|
def enable_mocktime():
|
||||||
|
#For backwared compatibility of the python scripts
|
||||||
|
#with previous versions of the cache, set MOCKTIME
|
||||||
|
#to Jan 1, 2014 + (201 * 10 * 60)
|
||||||
|
global MOCKTIME
|
||||||
|
MOCKTIME = 1414776313 + (201 * 10 * 60)
|
||||||
|
|
||||||
|
def set_mocktime(t):
|
||||||
|
global MOCKTIME
|
||||||
|
MOCKTIME = t
|
||||||
|
|
||||||
|
def disable_mocktime():
|
||||||
|
global MOCKTIME
|
||||||
|
MOCKTIME = 0
|
||||||
|
|
||||||
|
def get_mocktime():
|
||||||
|
return MOCKTIME
|
||||||
|
|
||||||
|
def enable_coverage(dirname):
|
||||||
|
"""Maintain a log of which RPC calls are made during testing."""
|
||||||
|
global COVERAGE_DIR
|
||||||
|
COVERAGE_DIR = dirname
|
||||||
|
|
||||||
|
|
||||||
|
def get_rpc_proxy(url, node_number, timeout=None):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
url (str): URL of the RPC server to call
|
||||||
|
node_number (int): the node number (or id) that this calls to
|
||||||
|
|
||||||
|
Kwargs:
|
||||||
|
timeout (int): HTTP timeout in seconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
AuthServiceProxy. convenience object for making RPC calls.
|
||||||
|
|
||||||
|
"""
|
||||||
|
proxy_kwargs = {}
|
||||||
|
if timeout is not None:
|
||||||
|
proxy_kwargs['timeout'] = timeout
|
||||||
|
|
||||||
|
proxy = AuthServiceProxy(url, **proxy_kwargs)
|
||||||
|
proxy.url = url # store URL on proxy for info
|
||||||
|
|
||||||
|
coverage_logfile = coverage.get_filename(
|
||||||
|
COVERAGE_DIR, node_number) if COVERAGE_DIR else None
|
||||||
|
|
||||||
|
return coverage.AuthServiceProxyWrapper(proxy, coverage_logfile)
|
||||||
|
|
||||||
|
def get_evoznsync_status(node):
|
||||||
|
result = node.evoznsync("status")
|
||||||
|
return result['IsSynced']
|
||||||
|
|
||||||
|
def wait_to_sync(node, fast_znsync=False):
|
||||||
|
tm = 0
|
||||||
|
synced = False
|
||||||
|
while tm < 30:
|
||||||
|
synced = get_evoznsync_status(node)
|
||||||
|
if synced:
|
||||||
|
return
|
||||||
|
time.sleep(0.2)
|
||||||
|
if fast_znsync:
|
||||||
|
# skip mnsync states
|
||||||
|
node.evoznsync("next")
|
||||||
|
tm += 0.2
|
||||||
|
assert(synced)
|
||||||
|
|
||||||
|
def p2p_port(n):
|
||||||
|
assert(n <= MAX_NODES)
|
||||||
|
return PORT_MIN + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES)
|
||||||
|
|
||||||
|
def rpc_port(n):
|
||||||
|
return PORT_MIN + PORT_RANGE + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES)
|
||||||
|
|
||||||
|
def check_json_precision():
|
||||||
|
"""Make sure json library being used does not lose precision converting BTC values"""
|
||||||
|
n = Decimal("20000000.00000003")
|
||||||
|
satoshis = int(json.loads(json.dumps(float(n)))*1.0e8)
|
||||||
|
if satoshis != 2000000000000003:
|
||||||
|
raise RuntimeError("JSON encode/decode loses precision")
|
||||||
|
|
||||||
|
def count_bytes(hex_string):
|
||||||
|
return len(bytearray.fromhex(hex_string))
|
||||||
|
|
||||||
|
def bytes_to_hex_str(byte_str):
|
||||||
|
return hexlify(byte_str).decode('ascii')
|
||||||
|
|
||||||
|
def hex_str_to_bytes(hex_str):
|
||||||
|
return unhexlify(hex_str.encode('ascii'))
|
||||||
|
|
||||||
|
def str_to_b64str(string):
|
||||||
|
return b64encode(string.encode('utf-8')).decode('ascii')
|
||||||
|
|
||||||
|
def sync_blocks(rpc_connections, *, wait=1, timeout=60):
|
||||||
|
"""
|
||||||
|
Wait until everybody has the same tip.
|
||||||
|
|
||||||
|
sync_blocks needs to be called with an rpc_connections set that has least
|
||||||
|
one node already synced to the latest, stable tip, otherwise there's a
|
||||||
|
chance it might return before all nodes are stably synced.
|
||||||
|
"""
|
||||||
|
# Use getblockcount() instead of waitforblockheight() to determine the
|
||||||
|
# initial max height because the two RPCs look at different internal global
|
||||||
|
# variables (chainActive vs latestBlock) and the former gets updated
|
||||||
|
# earlier.
|
||||||
|
maxheight = max(x.getblockcount() for x in rpc_connections)
|
||||||
|
start_time = cur_time = time.time()
|
||||||
|
while cur_time <= start_time + timeout:
|
||||||
|
tips = [r.waitforblockheight(maxheight, int(wait * 1000)) for r in rpc_connections]
|
||||||
|
if all(t["height"] == maxheight for t in tips):
|
||||||
|
if all(t["hash"] == tips[0]["hash"] for t in tips):
|
||||||
|
return
|
||||||
|
raise AssertionError("Block sync failed, mismatched block hashes:{}".format(
|
||||||
|
"".join("\n {!r}".format(tip) for tip in tips)))
|
||||||
|
|
||||||
|
time.sleep(wait)
|
||||||
|
cur_time = time.time()
|
||||||
|
raise AssertionError("Block sync to height {} timed out:{}".format(
|
||||||
|
maxheight, "".join("\n {!r}".format(tip) for tip in tips)))
|
||||||
|
|
||||||
|
def sync_znodes(rpc_connections, *, timeout=60):
|
||||||
|
"""
|
||||||
|
Waits until every node has their znsync status is synced.
|
||||||
|
"""
|
||||||
|
start_time = cur_time = time.time()
|
||||||
|
while cur_time <= start_time + timeout:
|
||||||
|
statuses = [r.znsync("status") for r in rpc_connections]
|
||||||
|
if all(stat["IsSynced"] == True for stat in statuses):
|
||||||
|
return
|
||||||
|
cur_time = time.time()
|
||||||
|
raise AssertionError("Znode sync failed.")
|
||||||
|
|
||||||
|
def sync_chain(rpc_connections, *, wait=1, timeout=60):
|
||||||
|
"""
|
||||||
|
Wait until everybody has the same best block
|
||||||
|
"""
|
||||||
|
while timeout > 0:
|
||||||
|
best_hash = [x.getbestblockhash() for x in rpc_connections]
|
||||||
|
if best_hash == [best_hash[0]]*len(best_hash):
|
||||||
|
return
|
||||||
|
time.sleep(wait)
|
||||||
|
timeout -= wait
|
||||||
|
raise AssertionError("Chain sync failed: Best block hashes don't match")
|
||||||
|
|
||||||
|
def sync_mempools(rpc_connections, *, wait=1, timeout=60):
|
||||||
|
"""
|
||||||
|
Wait until everybody has the same transactions in their memory
|
||||||
|
pools
|
||||||
|
"""
|
||||||
|
while timeout > 0:
|
||||||
|
pool = set(rpc_connections[0].getrawmempool())
|
||||||
|
num_match = 1
|
||||||
|
for i in range(1, len(rpc_connections)):
|
||||||
|
if set(rpc_connections[i].getrawmempool()) == pool:
|
||||||
|
num_match = num_match+1
|
||||||
|
if num_match == len(rpc_connections):
|
||||||
|
return
|
||||||
|
time.sleep(wait)
|
||||||
|
timeout -= wait
|
||||||
|
raise AssertionError("Mempool sync failed")
|
||||||
|
|
||||||
|
def sync_znodes(rpc_connections, fast_mnsync=False):
|
||||||
|
for node in rpc_connections:
|
||||||
|
wait_to_sync(node, fast_mnsync)
|
||||||
|
|
||||||
|
bitcoind_processes = {}
|
||||||
|
|
||||||
|
def initialize_datadir(dirname, n):
|
||||||
|
datadir = os.path.join(dirname, "node"+str(n))
|
||||||
|
if not os.path.isdir(datadir):
|
||||||
|
os.makedirs(datadir)
|
||||||
|
rpc_u, rpc_p = rpc_auth_pair(n)
|
||||||
|
with open(os.path.join(datadir, "firo.conf"), 'w', encoding='utf8') as f:
|
||||||
|
f.write("regtest=1\n")
|
||||||
|
f.write("rpcuser=" + rpc_u + "\n")
|
||||||
|
f.write("rpcpassword=" + rpc_p + "\n")
|
||||||
|
f.write("port="+str(p2p_port(n))+"\n")
|
||||||
|
f.write("rpcport="+str(rpc_port(n))+"\n")
|
||||||
|
f.write("listenonion=0\n")
|
||||||
|
return datadir
|
||||||
|
|
||||||
|
def rpc_auth_pair(n):
|
||||||
|
return 'rpcuser💻' + str(n), 'rpcpass🔑' + str(n)
|
||||||
|
|
||||||
|
def rpc_url(i, rpchost=None):
|
||||||
|
rpc_u, rpc_p = rpc_auth_pair(i)
|
||||||
|
host = '127.0.0.1'
|
||||||
|
port = rpc_port(i)
|
||||||
|
if rpchost:
|
||||||
|
parts = rpchost.split(':')
|
||||||
|
if len(parts) == 2:
|
||||||
|
host, port = parts
|
||||||
|
else:
|
||||||
|
host = rpchost
|
||||||
|
return "http://%s:%s@%s:%d" % (rpc_u, rpc_p, host, int(port))
|
||||||
|
|
||||||
|
def wait_for_bitcoind_start(process, url, i):
|
||||||
|
'''
|
||||||
|
Wait for firod to start. This means that RPC is accessible and fully initialized.
|
||||||
|
Raise an exception if firod exits during initialization.
|
||||||
|
'''
|
||||||
|
while True:
|
||||||
|
if process.poll() is not None:
|
||||||
|
raise Exception('firod exited with status %i during initialization' % process.returncode)
|
||||||
|
try:
|
||||||
|
rpc = get_rpc_proxy(url, i)
|
||||||
|
blocks = rpc.getblockcount()
|
||||||
|
break # break out of loop on success
|
||||||
|
except IOError as e:
|
||||||
|
if e.errno != errno.ECONNREFUSED: # Port not yet open?
|
||||||
|
raise # unknown IO error
|
||||||
|
except JSONRPCException as e: # Initialization phase
|
||||||
|
if e.error['code'] != -28: # RPC in warmup?
|
||||||
|
raise # unknown JSON RPC exception
|
||||||
|
time.sleep(0.25)
|
||||||
|
|
||||||
|
def initialize_chain(test_dir, num_nodes, cachedir):
|
||||||
|
"""
|
||||||
|
Create a cache of a 200-block-long chain (with wallet) for MAX_NODES
|
||||||
|
Afterward, create num_nodes copies from the cache
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert num_nodes <= MAX_NODES
|
||||||
|
create_cache = False
|
||||||
|
for i in range(MAX_NODES):
|
||||||
|
if not os.path.isdir(os.path.join(cachedir, 'node'+str(i))):
|
||||||
|
create_cache = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if create_cache:
|
||||||
|
|
||||||
|
#find and delete old cache directories if any exist
|
||||||
|
for i in range(MAX_NODES):
|
||||||
|
if os.path.isdir(os.path.join(cachedir,"node"+str(i))):
|
||||||
|
shutil.rmtree(os.path.join(cachedir,"node"+str(i)))
|
||||||
|
|
||||||
|
# Create cache directories, run bitcoinds:
|
||||||
|
for i in range(MAX_NODES):
|
||||||
|
datadir=initialize_datadir(cachedir, i)
|
||||||
|
args = [ os.getenv("FIROD", "firod"), "-server", "-keypool=1", "-datadir="+datadir, "-discover=0" ]
|
||||||
|
if i > 0:
|
||||||
|
args.append("-connect=127.0.0.1:"+str(p2p_port(0)))
|
||||||
|
bitcoind_processes[i] = subprocess.Popen(args)
|
||||||
|
if os.getenv("PYTHON_DEBUG", ""):
|
||||||
|
print("initialize_chain: bitcoind started, waiting for RPC to come up")
|
||||||
|
wait_for_bitcoind_start(bitcoind_processes[i], rpc_url(i), i)
|
||||||
|
if os.getenv("PYTHON_DEBUG", ""):
|
||||||
|
print("initialize_chain: RPC successfully started")
|
||||||
|
|
||||||
|
rpcs = []
|
||||||
|
for i in range(MAX_NODES):
|
||||||
|
try:
|
||||||
|
rpcs.append(get_rpc_proxy(rpc_url(i), i))
|
||||||
|
except:
|
||||||
|
sys.stderr.write("Error connecting to "+url+"\n")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Create a 200-block-long chain; each of the 4 first nodes
|
||||||
|
# gets 25 mature blocks and 25 immature.
|
||||||
|
# Note: To preserve compatibility with older versions of
|
||||||
|
# initialize_chain, only 4 nodes will generate coins.
|
||||||
|
#
|
||||||
|
# blocks are created with timestamps 10 minutes apart
|
||||||
|
# starting from 2010 minutes in the past
|
||||||
|
enable_mocktime()
|
||||||
|
block_time = get_mocktime() - (201 * 10 * 60)
|
||||||
|
for i in range(2):
|
||||||
|
for peer in range(4):
|
||||||
|
for j in range(25):
|
||||||
|
set_node_times(rpcs, block_time)
|
||||||
|
rpcs[peer].generate(1)
|
||||||
|
block_time += 10*60
|
||||||
|
# Must sync before next peer starts generating blocks
|
||||||
|
sync_blocks(rpcs)
|
||||||
|
|
||||||
|
# Shut them down, and clean up cache directories:
|
||||||
|
stop_nodes(rpcs)
|
||||||
|
disable_mocktime()
|
||||||
|
for i in range(MAX_NODES):
|
||||||
|
try:
|
||||||
|
os.remove(log_filename(cachedir, i, "debug.log"))
|
||||||
|
os.remove(log_filename(cachedir, i, "db.log"))
|
||||||
|
os.remove(log_filename(cachedir, i, "peers.dat"))
|
||||||
|
os.remove(log_filename(cachedir, i, "fee_estimates.dat"))
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for i in range(num_nodes):
|
||||||
|
from_dir = os.path.join(cachedir, "node"+str(i))
|
||||||
|
to_dir = os.path.join(test_dir, "node"+str(i))
|
||||||
|
if from_dir != to_dir:
|
||||||
|
shutil.copytree(from_dir, to_dir)
|
||||||
|
initialize_datadir(test_dir, i) # Overwrite port/rpcport in bitcoin.conf
|
||||||
|
|
||||||
|
def initialize_chain_clean(test_dir, num_nodes):
|
||||||
|
"""
|
||||||
|
Create an empty blockchain and num_nodes wallets.
|
||||||
|
Useful if a test case wants complete control over initialization.
|
||||||
|
"""
|
||||||
|
for i in range(num_nodes):
|
||||||
|
datadir=initialize_datadir(test_dir, i)
|
||||||
|
|
||||||
|
|
||||||
|
def _rpchost_to_args(rpchost):
|
||||||
|
'''Convert optional IP:port spec to rpcconnect/rpcport args'''
|
||||||
|
if rpchost is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
match = re.match('(\[[0-9a-fA-f:]+\]|[^:]+)(?::([0-9]+))?$', rpchost)
|
||||||
|
if not match:
|
||||||
|
raise ValueError('Invalid RPC host spec ' + rpchost)
|
||||||
|
|
||||||
|
rpcconnect = match.group(1)
|
||||||
|
rpcport = match.group(2)
|
||||||
|
|
||||||
|
if rpcconnect.startswith('['): # remove IPv6 [...] wrapping
|
||||||
|
rpcconnect = rpcconnect[1:-1]
|
||||||
|
|
||||||
|
rv = ['-rpcconnect=' + rpcconnect]
|
||||||
|
if rpcport:
|
||||||
|
rv += ['-rpcport=' + rpcport]
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def start_node(i, dirname, extra_args=None, rpchost=None, timewait=None, binary=None, redirect_stderr=False, stderr=None):
|
||||||
|
"""
|
||||||
|
Start a bitcoind and return RPC connection to it
|
||||||
|
"""
|
||||||
|
datadir = os.path.join(dirname, "node"+str(i))
|
||||||
|
if binary is None:
|
||||||
|
binary = os.getenv("FIROD", "firod")
|
||||||
|
args = [ binary, "-datadir="+datadir, "-server", "-keypool=1", "-discover=0", "-rest", "-dandelion=0", "-usemnemonic=0", "-mocktime="+str(get_mocktime()) ]
|
||||||
|
#Useful args for debugging
|
||||||
|
# "screen", "--",
|
||||||
|
# "gdb", "-x", "/tmp/gdb_run", "--args",
|
||||||
|
|
||||||
|
# Don't try auto backups (they fail a lot when running tests)
|
||||||
|
args += [ "-createwalletbackups=0" ]
|
||||||
|
if extra_args is not None: args.extend(extra_args)
|
||||||
|
# Allow to redirect stderr to stdout in case we expect some non-critical warnings/errors printed to stderr
|
||||||
|
# Otherwise the whole test would be considered to be failed in such cases
|
||||||
|
if redirect_stderr:
|
||||||
|
stderr = sys.stdout
|
||||||
|
bitcoind_processes[i] = subprocess.Popen(args, stderr=stderr)
|
||||||
|
logger.debug("start_node: firod started, waiting for RPC to come up")
|
||||||
|
url = rpc_url(i, rpchost)
|
||||||
|
wait_for_bitcoind_start(bitcoind_processes[i], url, i)
|
||||||
|
logger.debug("start_node: RPC successfully started")
|
||||||
|
proxy = get_rpc_proxy(url, i, timeout=timewait)
|
||||||
|
|
||||||
|
if COVERAGE_DIR:
|
||||||
|
coverage.write_all_rpc_commands(COVERAGE_DIR, proxy)
|
||||||
|
|
||||||
|
return proxy
|
||||||
|
|
||||||
|
def start_nodes(num_nodes, dirname, extra_args=None, rpchost=None, timewait=None, binary=None):
|
||||||
|
"""
|
||||||
|
Start multiple bitcoinds, return RPC connections to them
|
||||||
|
"""
|
||||||
|
if extra_args is None: extra_args = [ None for _ in range(num_nodes) ]
|
||||||
|
if binary is None: binary = [ None for _ in range(num_nodes) ]
|
||||||
|
rpcs = []
|
||||||
|
try:
|
||||||
|
for i in range(num_nodes):
|
||||||
|
rpcs.append(start_node(i, dirname, extra_args[i], rpchost, timewait=timewait, binary=binary[i]))
|
||||||
|
except: # If one node failed to start, stop the others
|
||||||
|
stop_nodes(rpcs)
|
||||||
|
raise
|
||||||
|
return rpcs
|
||||||
|
|
||||||
|
def copy_datadir(from_node, to_node, dirname):
|
||||||
|
from_datadir = os.path.join(dirname, "node"+str(from_node), "regtest")
|
||||||
|
to_datadir = os.path.join(dirname, "node"+str(to_node), "regtest")
|
||||||
|
|
||||||
|
dirs = ["blocks", "chainstate", "evodb", "llmq"]
|
||||||
|
for d in dirs:
|
||||||
|
try:
|
||||||
|
src = os.path.join(from_datadir, d)
|
||||||
|
dst = os.path.join(to_datadir, d)
|
||||||
|
shutil.copytree(src, dst)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
def log_filename(dirname, n_node, logname):
|
||||||
|
return os.path.join(dirname, "node"+str(n_node), "regtest", logname)
|
||||||
|
|
||||||
|
def wait_node(i):
|
||||||
|
return_code = bitcoind_processes[i].wait(timeout=BITCOIND_PROC_WAIT_TIMEOUT)
|
||||||
|
assert_equal(return_code, 0)
|
||||||
|
del bitcoind_processes[i]
|
||||||
|
|
||||||
|
def stop_node(node, i, wait=True):
|
||||||
|
logger.debug("Stopping node %d" % i)
|
||||||
|
try:
|
||||||
|
node.stop()
|
||||||
|
except http.client.CannotSendRequest as e:
|
||||||
|
logger.exception("Unable to stop node")
|
||||||
|
if wait:
|
||||||
|
wait_node(i)
|
||||||
|
|
||||||
|
def stop_nodes(nodes, fast=True):
|
||||||
|
for i, node in enumerate(nodes):
|
||||||
|
stop_node(node, i, not fast)
|
||||||
|
if fast:
|
||||||
|
for i, node in enumerate(nodes):
|
||||||
|
wait_node(i)
|
||||||
|
assert not bitcoind_processes.values() # All connections must be gone now
|
||||||
|
|
||||||
|
def set_node_times(nodes, t):
|
||||||
|
for node in nodes:
|
||||||
|
node.setmocktime(t)
|
||||||
|
|
||||||
|
def connect_nodes(from_connection, node_num):
|
||||||
|
# NOTE: In next line p2p_port(0) was replaced by rpc_port(0).
|
||||||
|
ip_port = "127.0.0.1:"+str(p2p_port(node_num))
|
||||||
|
from_connection.addnode(ip_port, "onetry")
|
||||||
|
# poll until version handshake complete to avoid race conditions
|
||||||
|
# with transaction relaying
|
||||||
|
while any(peer['version'] == 0 for peer in from_connection.getpeerinfo()):
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
def connect_nodes_bi(nodes, a, b):
|
||||||
|
connect_nodes(nodes[a], b)
|
||||||
|
connect_nodes(nodes[b], a)
|
||||||
|
|
||||||
|
def isolate_node(node, timeout=5):
|
||||||
|
node.setnetworkactive(False)
|
||||||
|
st = time.time()
|
||||||
|
while time.time() < st + timeout:
|
||||||
|
if node.getconnectioncount() == 0:
|
||||||
|
return
|
||||||
|
time.sleep(0.5)
|
||||||
|
raise AssertionError("disconnect_node timed out")
|
||||||
|
|
||||||
|
def reconnect_isolated_node(node, node_num):
|
||||||
|
node.setnetworkactive(True)
|
||||||
|
connect_nodes(node, node_num)
|
||||||
|
def find_output(node, txid, amount):
|
||||||
|
"""
|
||||||
|
Return index to output of txid with value amount
|
||||||
|
Raises exception if there is none.
|
||||||
|
"""
|
||||||
|
txdata = node.getrawtransaction(txid, 1)
|
||||||
|
for i in range(len(txdata["vout"])):
|
||||||
|
if txdata["vout"][i]["value"] == amount:
|
||||||
|
return i
|
||||||
|
raise RuntimeError("find_output txid %s : %s not found"%(txid,str(amount)))
|
||||||
|
|
||||||
|
|
||||||
|
def gather_inputs(from_node, amount_needed, confirmations_required=1):
|
||||||
|
"""
|
||||||
|
Return a random set of unspent txouts that are enough to pay amount_needed
|
||||||
|
"""
|
||||||
|
assert(confirmations_required >=0)
|
||||||
|
utxo = from_node.listunspent(confirmations_required)
|
||||||
|
random.shuffle(utxo)
|
||||||
|
inputs = []
|
||||||
|
total_in = Decimal("0.00000000")
|
||||||
|
while total_in < amount_needed and len(utxo) > 0:
|
||||||
|
t = utxo.pop()
|
||||||
|
total_in += t["amount"]
|
||||||
|
inputs.append({ "txid" : t["txid"], "vout" : t["vout"], "address" : t["address"] } )
|
||||||
|
if total_in < amount_needed:
|
||||||
|
raise RuntimeError("Insufficient funds: need %d, have %d"%(amount_needed, total_in))
|
||||||
|
return (total_in, inputs)
|
||||||
|
|
||||||
|
def make_change(from_node, amount_in, amount_out, fee):
|
||||||
|
"""
|
||||||
|
Create change output(s), return them
|
||||||
|
"""
|
||||||
|
outputs = {}
|
||||||
|
amount = amount_out+fee
|
||||||
|
change = amount_in - amount
|
||||||
|
if change > amount*2:
|
||||||
|
# Create an extra change output to break up big inputs
|
||||||
|
change_address = from_node.getnewaddress()
|
||||||
|
# Split change in two, being careful of rounding:
|
||||||
|
outputs[change_address] = Decimal(change/2).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN)
|
||||||
|
change = amount_in - amount - outputs[change_address]
|
||||||
|
if change > 0:
|
||||||
|
outputs[from_node.getnewaddress()] = change
|
||||||
|
return outputs
|
||||||
|
|
||||||
|
def send_zeropri_transaction(from_node, to_node, amount, fee):
|
||||||
|
"""
|
||||||
|
Create&broadcast a zero-priority transaction.
|
||||||
|
Returns (txid, hex-encoded-txdata)
|
||||||
|
Ensures transaction is zero-priority by first creating a send-to-self,
|
||||||
|
then using its output
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create a send-to-self with confirmed inputs:
|
||||||
|
self_address = from_node.getnewaddress()
|
||||||
|
(total_in, inputs) = gather_inputs(from_node, amount+fee*2)
|
||||||
|
outputs = make_change(from_node, total_in, amount+fee, fee)
|
||||||
|
outputs[self_address] = float(amount+fee)
|
||||||
|
|
||||||
|
self_rawtx = from_node.createrawtransaction(inputs, outputs)
|
||||||
|
self_signresult = from_node.signrawtransaction(self_rawtx)
|
||||||
|
self_txid = from_node.sendrawtransaction(self_signresult["hex"], True)
|
||||||
|
|
||||||
|
vout = find_output(from_node, self_txid, amount+fee)
|
||||||
|
# Now immediately spend the output to create a 1-input, 1-output
|
||||||
|
# zero-priority transaction:
|
||||||
|
inputs = [ { "txid" : self_txid, "vout" : vout } ]
|
||||||
|
outputs = { to_node.getnewaddress() : float(amount) }
|
||||||
|
|
||||||
|
rawtx = from_node.createrawtransaction(inputs, outputs)
|
||||||
|
signresult = from_node.signrawtransaction(rawtx)
|
||||||
|
txid = from_node.sendrawtransaction(signresult["hex"], True)
|
||||||
|
|
||||||
|
return (txid, signresult["hex"])
|
||||||
|
|
||||||
|
def random_zeropri_transaction(nodes, amount, min_fee, fee_increment, fee_variants):
|
||||||
|
"""
|
||||||
|
Create a random zero-priority transaction.
|
||||||
|
Returns (txid, hex-encoded-transaction-data, fee)
|
||||||
|
"""
|
||||||
|
from_node = random.choice(nodes)
|
||||||
|
to_node = random.choice(nodes)
|
||||||
|
fee = min_fee + fee_increment*random.randint(0,fee_variants)
|
||||||
|
(txid, txhex) = send_zeropri_transaction(from_node, to_node, amount, fee)
|
||||||
|
return (txid, txhex, fee)
|
||||||
|
|
||||||
|
def random_transaction(nodes, amount, min_fee, fee_increment, fee_variants):
|
||||||
|
"""
|
||||||
|
Create a random transaction.
|
||||||
|
Returns (txid, hex-encoded-transaction-data, fee)
|
||||||
|
"""
|
||||||
|
from_node = random.choice(nodes)
|
||||||
|
to_node = random.choice(nodes)
|
||||||
|
fee = min_fee + fee_increment*random.randint(0,fee_variants)
|
||||||
|
|
||||||
|
(total_in, inputs) = gather_inputs(from_node, amount+fee)
|
||||||
|
outputs = make_change(from_node, total_in, amount, fee)
|
||||||
|
outputs[to_node.getnewaddress()] = float(amount)
|
||||||
|
|
||||||
|
rawtx = from_node.createrawtransaction(inputs, outputs)
|
||||||
|
signresult = from_node.signrawtransaction(rawtx)
|
||||||
|
txid = from_node.sendrawtransaction(signresult["hex"], True)
|
||||||
|
|
||||||
|
return (txid, signresult["hex"], fee)
|
||||||
|
|
||||||
|
def assert_fee_amount(fee, tx_size, fee_per_kB):
|
||||||
|
"""Assert the fee was in range"""
|
||||||
|
target_fee = tx_size * fee_per_kB / 1000
|
||||||
|
if fee < target_fee:
|
||||||
|
raise AssertionError("Fee of %s BTC too low! (Should be %s BTC)"%(str(fee), str(target_fee)))
|
||||||
|
# allow the wallet's estimation to be at most 2 bytes off
|
||||||
|
if fee > (tx_size + 2) * fee_per_kB / 1000:
|
||||||
|
raise AssertionError("Fee of %s BTC too high! (Should be %s BTC)"%(str(fee), str(target_fee)))
|
||||||
|
|
||||||
|
def assert_equal(thing1, thing2, *args):
|
||||||
|
if thing1 != thing2 or any(thing1 != arg for arg in args):
|
||||||
|
raise AssertionError("not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + args))
|
||||||
|
|
||||||
|
def assert_greater_than(thing1, thing2):
|
||||||
|
if thing1 <= thing2:
|
||||||
|
raise AssertionError("%s <= %s"%(str(thing1),str(thing2)))
|
||||||
|
|
||||||
|
def assert_greater_than_or_equal(thing1, thing2):
|
||||||
|
if thing1 < thing2:
|
||||||
|
raise AssertionError("%s < %s"%(str(thing1),str(thing2)))
|
||||||
|
|
||||||
|
def assert_raises(exc, fun, *args, **kwds):
|
||||||
|
assert_raises_message(exc, None, fun, *args, **kwds)
|
||||||
|
|
||||||
|
def assert_raises_message(exc, message, fun, *args, **kwds):
|
||||||
|
try:
|
||||||
|
fun(*args, **kwds)
|
||||||
|
except exc as e:
|
||||||
|
if message is not None and message not in e.error['message']:
|
||||||
|
raise AssertionError("Expected substring not found:"+e.error['message'])
|
||||||
|
except Exception as e:
|
||||||
|
raise AssertionError("Unexpected exception raised: "+type(e).__name__)
|
||||||
|
else:
|
||||||
|
raise AssertionError("No exception raised")
|
||||||
|
|
||||||
|
def assert_raises_jsonrpc(code, message, fun, *args, **kwds):
|
||||||
|
"""Run an RPC and verify that a specific JSONRPC exception code and message is raised.
|
||||||
|
|
||||||
|
Calls function `fun` with arguments `args` and `kwds`. Catches a JSONRPCException
|
||||||
|
and verifies that the error code and message are as expected. Throws AssertionError if
|
||||||
|
no JSONRPCException was returned or if the error code/message are not as expected.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
code (int), optional: the error code returned by the RPC call (defined
|
||||||
|
in src/rpc/protocol.h). Set to None if checking the error code is not required.
|
||||||
|
message (string), optional: [a substring of] the error string returned by the
|
||||||
|
RPC call. Set to None if checking the error string is not required
|
||||||
|
fun (function): the function to call. This should be the name of an RPC.
|
||||||
|
args*: positional arguments for the function.
|
||||||
|
kwds**: named arguments for the function.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
fun(*args, **kwds)
|
||||||
|
except JSONRPCException as e:
|
||||||
|
# JSONRPCException was thrown as expected. Check the code and message values are correct.
|
||||||
|
if (code is not None) and (code != e.error["code"]):
|
||||||
|
raise AssertionError("Unexpected JSONRPC error code %i" % e.error["code"])
|
||||||
|
if (message is not None) and (message not in e.error['message']):
|
||||||
|
raise AssertionError("Expected substring not found:"+e.error['message'])
|
||||||
|
except Exception as e:
|
||||||
|
raise AssertionError("Unexpected exception raised: "+type(e).__name__)
|
||||||
|
else:
|
||||||
|
raise AssertionError("No exception raised")
|
||||||
|
|
||||||
|
def assert_is_hex_string(string):
|
||||||
|
try:
|
||||||
|
int(string, 16)
|
||||||
|
except Exception as e:
|
||||||
|
raise AssertionError(
|
||||||
|
"Couldn't interpret %r as hexadecimal; raised: %s" % (string, e))
|
||||||
|
|
||||||
|
def assert_is_hash_string(string, length=64):
|
||||||
|
if not isinstance(string, str):
|
||||||
|
raise AssertionError("Expected a string, got type %r" % type(string))
|
||||||
|
elif length and len(string) != length:
|
||||||
|
raise AssertionError(
|
||||||
|
"String of length %d expected; got %d" % (length, len(string)))
|
||||||
|
elif not re.match('[abcdef0-9]+$', string):
|
||||||
|
raise AssertionError(
|
||||||
|
"String %r contains invalid characters for a hash." % string)
|
||||||
|
|
||||||
|
def assert_array_result(object_array, to_match, expected, should_not_find = False):
|
||||||
|
"""
|
||||||
|
Pass in array of JSON objects, a dictionary with key/value pairs
|
||||||
|
to match against, and another dictionary with expected key/value
|
||||||
|
pairs.
|
||||||
|
If the should_not_find flag is true, to_match should not be found
|
||||||
|
in object_array
|
||||||
|
"""
|
||||||
|
if should_not_find == True:
|
||||||
|
assert_equal(expected, { })
|
||||||
|
num_matched = 0
|
||||||
|
for item in object_array:
|
||||||
|
all_match = True
|
||||||
|
for key,value in to_match.items():
|
||||||
|
if item[key] != value:
|
||||||
|
all_match = False
|
||||||
|
if not all_match:
|
||||||
|
continue
|
||||||
|
elif should_not_find == True:
|
||||||
|
num_matched = num_matched+1
|
||||||
|
for key,value in expected.items():
|
||||||
|
if item[key] != value:
|
||||||
|
raise AssertionError("%s : expected %s=%s"%(str(item), str(key), str(value)))
|
||||||
|
num_matched = num_matched+1
|
||||||
|
if num_matched == 0 and should_not_find != True:
|
||||||
|
raise AssertionError("No objects matched %s"%(str(to_match)))
|
||||||
|
if num_matched > 0 and should_not_find == True:
|
||||||
|
raise AssertionError("Objects were found %s"%(str(to_match)))
|
||||||
|
|
||||||
|
def satoshi_round(amount):
|
||||||
|
return Decimal(amount).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN)
|
||||||
|
|
||||||
|
# Helper to create at least "count" utxos
|
||||||
|
# Pass in a fee that is sufficient for relay and mining new transactions.
|
||||||
|
def create_confirmed_utxos(fee, node, count):
|
||||||
|
node.generate(int(0.5*count)+101)
|
||||||
|
utxos = node.listunspent()
|
||||||
|
iterations = count - len(utxos)
|
||||||
|
addr1 = node.getnewaddress()
|
||||||
|
addr2 = node.getnewaddress()
|
||||||
|
if iterations <= 0:
|
||||||
|
return utxos
|
||||||
|
for i in range(iterations):
|
||||||
|
t = utxos.pop()
|
||||||
|
inputs = []
|
||||||
|
inputs.append({ "txid" : t["txid"], "vout" : t["vout"]})
|
||||||
|
outputs = {}
|
||||||
|
send_value = t['amount'] - fee
|
||||||
|
outputs[addr1] = satoshi_round(send_value/2)
|
||||||
|
outputs[addr2] = satoshi_round(send_value/2)
|
||||||
|
raw_tx = node.createrawtransaction(inputs, outputs)
|
||||||
|
signed_tx = node.signrawtransaction(raw_tx)["hex"]
|
||||||
|
txid = node.sendrawtransaction(signed_tx)
|
||||||
|
|
||||||
|
while (node.getmempoolinfo()['size'] > 0):
|
||||||
|
node.generate(1)
|
||||||
|
|
||||||
|
utxos = node.listunspent()
|
||||||
|
assert(len(utxos) >= count)
|
||||||
|
return utxos
|
||||||
|
|
||||||
|
# Create large OP_RETURN txouts that can be appended to a transaction
|
||||||
|
# to make it large (helper for constructing large transactions).
|
||||||
|
def gen_return_txouts():
|
||||||
|
# Some pre-processing to create a bunch of OP_RETURN txouts to insert into transactions we create
|
||||||
|
# So we have big transactions (and therefore can't fit very many into each block)
|
||||||
|
# create one script_pubkey
|
||||||
|
script_pubkey = "6a4d0200" #OP_RETURN OP_PUSH2 512 bytes
|
||||||
|
for i in range (512):
|
||||||
|
script_pubkey = script_pubkey + "01"
|
||||||
|
# concatenate 128 txouts of above script_pubkey which we'll insert before the txout for change
|
||||||
|
txouts = "81"
|
||||||
|
for k in range(128):
|
||||||
|
# add txout value
|
||||||
|
txouts = txouts + "0000000000000000"
|
||||||
|
# add length of script_pubkey
|
||||||
|
txouts = txouts + "fd0402"
|
||||||
|
# add script_pubkey
|
||||||
|
txouts = txouts + script_pubkey
|
||||||
|
return txouts
|
||||||
|
|
||||||
|
def create_tx(node, coinbase, to_address, amount):
|
||||||
|
inputs = [{ "txid" : coinbase, "vout" : 0}]
|
||||||
|
outputs = { to_address : amount }
|
||||||
|
rawtx = node.createrawtransaction(inputs, outputs)
|
||||||
|
signresult = node.signrawtransaction(rawtx)
|
||||||
|
assert_equal(signresult["complete"], True)
|
||||||
|
return signresult["hex"]
|
||||||
|
|
||||||
|
def create_tx_multi_input(node, inputs, outputs):
|
||||||
|
rawtx = node.createrawtransaction(inputs, outputs)
|
||||||
|
signresult = node.signrawtransaction(rawtx)
|
||||||
|
assert_equal(signresult["complete"], True)
|
||||||
|
return signresult["hex"]
|
||||||
|
|
||||||
|
# Create a spend of each passed-in utxo, splicing in "txouts" to each raw
|
||||||
|
# transaction to make it large. See gen_return_txouts() above.
|
||||||
|
def create_lots_of_big_transactions(node, txouts, utxos, num, fee):
|
||||||
|
addr = node.getnewaddress()
|
||||||
|
txids = []
|
||||||
|
for _ in range(num):
|
||||||
|
t = utxos.pop()
|
||||||
|
inputs=[{ "txid" : t["txid"], "vout" : t["vout"]}]
|
||||||
|
outputs = {}
|
||||||
|
change = t['amount'] - fee
|
||||||
|
outputs[addr] = satoshi_round(change)
|
||||||
|
rawtx = node.createrawtransaction(inputs, outputs)
|
||||||
|
newtx = rawtx[0:92]
|
||||||
|
newtx = newtx + txouts
|
||||||
|
newtx = newtx + rawtx[94:]
|
||||||
|
signresult = node.signrawtransaction(newtx, None, None, "NONE")
|
||||||
|
txid = node.sendrawtransaction(signresult["hex"], True)
|
||||||
|
txids.append(txid)
|
||||||
|
return txids
|
||||||
|
|
||||||
|
def mine_large_block(node, utxos=None):
|
||||||
|
# generate a 66k transaction,
|
||||||
|
# and 14 of them is close to the 1MB block limit
|
||||||
|
num = 14
|
||||||
|
txouts = gen_return_txouts()
|
||||||
|
utxos = utxos if utxos is not None else []
|
||||||
|
if len(utxos) < num:
|
||||||
|
utxos.clear()
|
||||||
|
utxos.extend(node.listunspent())
|
||||||
|
fee = 100 * node.getnetworkinfo()["relayfee"]
|
||||||
|
create_lots_of_big_transactions(node, txouts, utxos, num, fee=fee)
|
||||||
|
node.generate(1)
|
||||||
|
|
||||||
|
def get_bip9_status(node, key):
|
||||||
|
info = node.getblockchaininfo()
|
||||||
|
return info['bip9_softforks'][key]
|
||||||
|
|
||||||
|
def dumpprivkey_otac(node, address):
|
||||||
|
import re
|
||||||
|
error_text = ''
|
||||||
|
try:
|
||||||
|
return node.dumpprivkey(address)
|
||||||
|
except JSONRPCException as e:
|
||||||
|
error_text = e.error
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
otac_match = re.search("Your one time authorization code is: ([a-zA-Z0-9]+)", error_text['message'])
|
||||||
|
if not otac_match:
|
||||||
|
raise JSONRPCException(error_text)
|
||||||
|
return node.dumpprivkey(address, otac_match.groups()[0])
|
||||||
|
|
||||||
|
def get_znsync_status(node):
|
||||||
|
result = node.znsync("status")
|
||||||
|
return result['IsSynced']
|
||||||
|
|
||||||
|
def wait_to_sync_znodes(node, fast_znsync=False):
|
||||||
|
while True:
|
||||||
|
synced = get_znsync_status(node)
|
||||||
|
if synced:
|
||||||
|
break
|
||||||
|
time.sleep(0.2)
|
||||||
|
if fast_znsync:
|
||||||
|
# skip mnsync states
|
||||||
|
node.znsync("next")
|
||||||
|
|
||||||
|
def get_full_balance(node):
|
||||||
|
wallet_info = node.getwalletinfo()
|
||||||
|
return wallet_info["balance"] + wallet_info["immature_balance"] + wallet_info["unconfirmed_balance"]
|
||||||
0
basicswap/interface/contrib/nav_test_framework/__init__.py
Executable file
175
basicswap/interface/contrib/nav_test_framework/authproxy.py
Executable file
@@ -0,0 +1,175 @@
|
|||||||
|
|
||||||
|
"""
|
||||||
|
Copyright 2011 Jeff Garzik
|
||||||
|
|
||||||
|
AuthServiceProxy has the following improvements over python-jsonrpc's
|
||||||
|
ServiceProxy class:
|
||||||
|
|
||||||
|
- HTTP connections persist for the life of the AuthServiceProxy object
|
||||||
|
(if server supports HTTP/1.1)
|
||||||
|
- sends protocol 'version', per JSON-RPC 1.1
|
||||||
|
- sends proper, incrementing 'id'
|
||||||
|
- sends Basic HTTP authentication headers
|
||||||
|
- parses all JSON numbers that look like floats as Decimal
|
||||||
|
- uses standard Python json lib
|
||||||
|
|
||||||
|
Previous copyright, from python-jsonrpc/jsonrpc/proxy.py:
|
||||||
|
|
||||||
|
Copyright (c) 2007 Jan-Klaas Kollhof
|
||||||
|
|
||||||
|
This file is part of jsonrpc.
|
||||||
|
|
||||||
|
jsonrpc is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2.1 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This software is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with this software; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
import http.client as httplib
|
||||||
|
except ImportError:
|
||||||
|
import httplib
|
||||||
|
import base64
|
||||||
|
import decimal
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
try:
|
||||||
|
import urllib.parse as urlparse
|
||||||
|
except ImportError:
|
||||||
|
import urlparse
|
||||||
|
|
||||||
|
USER_AGENT = "AuthServiceProxy/0.1"
|
||||||
|
|
||||||
|
HTTP_TIMEOUT = 30
|
||||||
|
|
||||||
|
log = logging.getLogger("NavcoinRPC")
|
||||||
|
|
||||||
|
class JSONRPCException(Exception):
|
||||||
|
def __init__(self, rpc_error):
|
||||||
|
Exception.__init__(self)
|
||||||
|
self.error = rpc_error
|
||||||
|
|
||||||
|
|
||||||
|
def EncodeDecimal(o):
|
||||||
|
if isinstance(o, decimal.Decimal):
|
||||||
|
return str(o)
|
||||||
|
raise TypeError(repr(o) + " is not JSON serializable")
|
||||||
|
|
||||||
|
class AuthServiceProxy(object):
|
||||||
|
__id_count = 0
|
||||||
|
|
||||||
|
# ensure_ascii: escape unicode as \uXXXX, passed to json.dumps
|
||||||
|
def __init__(self, service_url, service_name=None, timeout=HTTP_TIMEOUT, connection=None, ensure_ascii=True):
|
||||||
|
self.__service_url = service_url
|
||||||
|
self._service_name = service_name
|
||||||
|
self.ensure_ascii = ensure_ascii # can be toggled on the fly by tests
|
||||||
|
self.__url = urlparse.urlparse(service_url)
|
||||||
|
if self.__url.port is None:
|
||||||
|
port = 80
|
||||||
|
else:
|
||||||
|
port = self.__url.port
|
||||||
|
(user, passwd) = (self.__url.username, self.__url.password)
|
||||||
|
try:
|
||||||
|
user = user.encode('utf8')
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
passwd = passwd.encode('utf8')
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
authpair = user + b':' + passwd
|
||||||
|
self.__auth_header = b'Basic ' + base64.b64encode(authpair)
|
||||||
|
|
||||||
|
if connection:
|
||||||
|
# Callables re-use the connection of the original proxy
|
||||||
|
self.__conn = connection
|
||||||
|
elif self.__url.scheme == 'https':
|
||||||
|
self.__conn = httplib.HTTPSConnection(self.__url.hostname, port,
|
||||||
|
timeout=timeout)
|
||||||
|
else:
|
||||||
|
self.__conn = httplib.HTTPConnection(self.__url.hostname, port,
|
||||||
|
timeout=timeout)
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
if name.startswith('__') and name.endswith('__'):
|
||||||
|
# Python internal stuff
|
||||||
|
raise AttributeError
|
||||||
|
if self._service_name is not None:
|
||||||
|
name = "%s.%s" % (self._service_name, name)
|
||||||
|
return AuthServiceProxy(self.__service_url, name, connection=self.__conn)
|
||||||
|
|
||||||
|
def _request(self, method, path, postdata):
|
||||||
|
'''
|
||||||
|
Do a HTTP request, with retry if we get disconnected (e.g. due to a timeout).
|
||||||
|
This is a workaround for https://bugs.python.org/issue3566 which is fixed in Python 3.5.
|
||||||
|
'''
|
||||||
|
headers = {'Host': self.__url.hostname,
|
||||||
|
'User-Agent': USER_AGENT,
|
||||||
|
'Authorization': self.__auth_header,
|
||||||
|
'Content-type': 'application/json'}
|
||||||
|
try:
|
||||||
|
self.__conn.request(method, path, postdata, headers)
|
||||||
|
return self._get_response()
|
||||||
|
except httplib.BadStatusLine as e:
|
||||||
|
if e.line == "''": # if connection was closed, try again
|
||||||
|
self.__conn.close()
|
||||||
|
self.__conn.request(method, path, postdata, headers)
|
||||||
|
return self._get_response()
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
except BrokenPipeError:
|
||||||
|
# Python 3.5+ raises this instead of BadStatusLine when the connection was reset
|
||||||
|
self.__conn.close()
|
||||||
|
self.__conn.request(method, path, postdata, headers)
|
||||||
|
return self._get_response()
|
||||||
|
|
||||||
|
def __call__(self, *args):
|
||||||
|
AuthServiceProxy.__id_count += 1
|
||||||
|
|
||||||
|
log.debug("-%s-> %s %s"%(AuthServiceProxy.__id_count, self._service_name,
|
||||||
|
json.dumps(args, default=EncodeDecimal, ensure_ascii=self.ensure_ascii)))
|
||||||
|
postdata = json.dumps({'version': '1.1',
|
||||||
|
'method': self._service_name,
|
||||||
|
'params': args,
|
||||||
|
'id': AuthServiceProxy.__id_count}, default=EncodeDecimal, ensure_ascii=self.ensure_ascii)
|
||||||
|
response = self._request('POST', self.__url.path, postdata.encode('utf-8'))
|
||||||
|
if response['error'] is not None:
|
||||||
|
raise JSONRPCException(response['error'])
|
||||||
|
elif 'result' not in response:
|
||||||
|
raise JSONRPCException({
|
||||||
|
'code': -343, 'message': 'missing JSON-RPC result'})
|
||||||
|
else:
|
||||||
|
return response['result']
|
||||||
|
|
||||||
|
def _batch(self, rpc_call_list):
|
||||||
|
postdata = json.dumps(list(rpc_call_list), default=EncodeDecimal, ensure_ascii=self.ensure_ascii)
|
||||||
|
log.debug("--> "+postdata)
|
||||||
|
return self._request('POST', self.__url.path, postdata.encode('utf-8'))
|
||||||
|
|
||||||
|
def _get_response(self):
|
||||||
|
http_response = self.__conn.getresponse()
|
||||||
|
if http_response is None:
|
||||||
|
raise JSONRPCException({
|
||||||
|
'code': -342, 'message': 'missing HTTP response from server'})
|
||||||
|
|
||||||
|
content_type = http_response.getheader('Content-Type')
|
||||||
|
if content_type != 'application/json':
|
||||||
|
raise JSONRPCException({
|
||||||
|
'code': -342, 'message': 'non-JSON HTTP response with \'%i %s\' from server' % (http_response.status, http_response.reason)})
|
||||||
|
|
||||||
|
responsedata = http_response.read().decode('utf8')
|
||||||
|
response = json.loads(responsedata, parse_float=decimal.Decimal)
|
||||||
|
if "error" in response and response["error"] is None:
|
||||||
|
log.debug("<-%s- %s"%(response["id"], json.dumps(response["result"], default=EncodeDecimal, ensure_ascii=self.ensure_ascii)))
|
||||||
|
else:
|
||||||
|
log.debug("<-- "+responsedata)
|
||||||
|
return response
|
||||||
101
basicswap/interface/contrib/nav_test_framework/bignum.py
Executable file
@@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# bignum.py
|
||||||
|
#
|
||||||
|
# This file is copied from python-navcoinlib.
|
||||||
|
#
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
#
|
||||||
|
|
||||||
|
"""Bignum routines"""
|
||||||
|
|
||||||
|
|
||||||
|
import struct
|
||||||
|
|
||||||
|
|
||||||
|
# generic big endian MPI format
|
||||||
|
|
||||||
|
def bn_bytes(v, have_ext=False):
|
||||||
|
ext = 0
|
||||||
|
if have_ext:
|
||||||
|
ext = 1
|
||||||
|
return ((v.bit_length()+7)//8) + ext
|
||||||
|
|
||||||
|
def bn2bin(v):
|
||||||
|
s = bytearray()
|
||||||
|
i = bn_bytes(v)
|
||||||
|
while i > 0:
|
||||||
|
s.append((v >> ((i-1) * 8)) & 0xff)
|
||||||
|
i -= 1
|
||||||
|
return s
|
||||||
|
|
||||||
|
def bin2bn(s):
|
||||||
|
l = 0
|
||||||
|
for ch in s:
|
||||||
|
l = (l << 8) | ch
|
||||||
|
return l
|
||||||
|
|
||||||
|
def bn2mpi(v):
|
||||||
|
have_ext = False
|
||||||
|
if v.bit_length() > 0:
|
||||||
|
have_ext = (v.bit_length() & 0x07) == 0
|
||||||
|
|
||||||
|
neg = False
|
||||||
|
if v < 0:
|
||||||
|
neg = True
|
||||||
|
v = -v
|
||||||
|
|
||||||
|
s = struct.pack(b">I", bn_bytes(v, have_ext))
|
||||||
|
ext = bytearray()
|
||||||
|
if have_ext:
|
||||||
|
ext.append(0)
|
||||||
|
v_bin = bn2bin(v)
|
||||||
|
if neg:
|
||||||
|
if have_ext:
|
||||||
|
ext[0] |= 0x80
|
||||||
|
else:
|
||||||
|
v_bin[0] |= 0x80
|
||||||
|
return s + ext + v_bin
|
||||||
|
|
||||||
|
def mpi2bn(s):
|
||||||
|
if len(s) < 4:
|
||||||
|
return None
|
||||||
|
s_size = bytes(s[:4])
|
||||||
|
v_len = struct.unpack(b">I", s_size)[0]
|
||||||
|
if len(s) != (v_len + 4):
|
||||||
|
return None
|
||||||
|
if v_len == 0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
v_str = bytearray(s[4:])
|
||||||
|
neg = False
|
||||||
|
i = v_str[0]
|
||||||
|
if i & 0x80:
|
||||||
|
neg = True
|
||||||
|
i &= ~0x80
|
||||||
|
v_str[0] = i
|
||||||
|
|
||||||
|
v = bin2bn(v_str)
|
||||||
|
|
||||||
|
if neg:
|
||||||
|
return -v
|
||||||
|
return v
|
||||||
|
|
||||||
|
# navcoin-specific little endian format, with implicit size
|
||||||
|
def mpi2vch(s):
|
||||||
|
r = s[4:] # strip size
|
||||||
|
r = r[::-1] # reverse string, converting BE->LE
|
||||||
|
return r
|
||||||
|
|
||||||
|
def bn2vch(v):
|
||||||
|
return bytes(mpi2vch(bn2mpi(v)))
|
||||||
|
|
||||||
|
def vch2mpi(s):
|
||||||
|
r = struct.pack(b">I", len(s)) # size
|
||||||
|
r += s[::-1] # reverse string, converting LE->BE
|
||||||
|
return r
|
||||||
|
|
||||||
|
def vch2bn(s):
|
||||||
|
return mpi2bn(vch2mpi(s))
|
||||||
|
|
||||||
106
basicswap/interface/contrib/nav_test_framework/coverage.py
Executable file
@@ -0,0 +1,106 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright (c) 2015-2016 The Bitcoin Core developers
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
|
"""
|
||||||
|
This module contains utilities for doing coverage analysis on the RPC
|
||||||
|
interface.
|
||||||
|
|
||||||
|
It provides a way to track which RPC commands are exercised during
|
||||||
|
testing.
|
||||||
|
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
REFERENCE_FILENAME = 'rpc_interface.txt'
|
||||||
|
|
||||||
|
|
||||||
|
class AuthServiceProxyWrapper(object):
|
||||||
|
"""
|
||||||
|
An object that wraps AuthServiceProxy to record specific RPC calls.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, auth_service_proxy_instance, coverage_logfile=None):
|
||||||
|
"""
|
||||||
|
Kwargs:
|
||||||
|
auth_service_proxy_instance (AuthServiceProxy): the instance
|
||||||
|
being wrapped.
|
||||||
|
coverage_logfile (str): if specified, write each service_name
|
||||||
|
out to a file when called.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.auth_service_proxy_instance = auth_service_proxy_instance
|
||||||
|
self.coverage_logfile = coverage_logfile
|
||||||
|
|
||||||
|
def __getattr__(self, *args, **kwargs):
|
||||||
|
return_val = self.auth_service_proxy_instance.__getattr__(
|
||||||
|
*args, **kwargs)
|
||||||
|
|
||||||
|
return AuthServiceProxyWrapper(return_val, self.coverage_logfile)
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Delegates to AuthServiceProxy, then writes the particular RPC method
|
||||||
|
called to a file.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return_val = self.auth_service_proxy_instance.__call__(*args, **kwargs)
|
||||||
|
rpc_method = self.auth_service_proxy_instance._service_name
|
||||||
|
|
||||||
|
if self.coverage_logfile:
|
||||||
|
with open(self.coverage_logfile, 'a+') as f:
|
||||||
|
f.write("%s\n" % rpc_method)
|
||||||
|
|
||||||
|
return return_val
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self):
|
||||||
|
return self.auth_service_proxy_instance.url
|
||||||
|
|
||||||
|
|
||||||
|
def get_filename(dirname, n_node):
|
||||||
|
"""
|
||||||
|
Get a filename unique to the test process ID and node.
|
||||||
|
|
||||||
|
This file will contain a list of RPC commands covered.
|
||||||
|
"""
|
||||||
|
pid = str(os.getpid())
|
||||||
|
return os.path.join(
|
||||||
|
dirname, "coverage.pid%s.node%s.txt" % (pid, str(n_node)))
|
||||||
|
|
||||||
|
|
||||||
|
def write_all_rpc_commands(dirname, node):
|
||||||
|
"""
|
||||||
|
Write out a list of all RPC functions available in `navcoin-cli` for
|
||||||
|
coverage comparison. This will only happen once per coverage
|
||||||
|
directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dirname (str): temporary test dir
|
||||||
|
node (AuthServiceProxy): client
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool. if the RPC interface file was written.
|
||||||
|
|
||||||
|
"""
|
||||||
|
filename = os.path.join(dirname, REFERENCE_FILENAME)
|
||||||
|
|
||||||
|
if os.path.isfile(filename):
|
||||||
|
return False
|
||||||
|
|
||||||
|
help_output = node.help().split('\n')
|
||||||
|
commands = set()
|
||||||
|
|
||||||
|
for line in help_output:
|
||||||
|
line = line.strip()
|
||||||
|
|
||||||
|
# Ignore blanks and headers
|
||||||
|
if line and not line.startswith('='):
|
||||||
|
commands.add("%s\n" % line.split()[0])
|
||||||
|
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
f.writelines(list(commands))
|
||||||
|
|
||||||
|
return True
|
||||||
1495
basicswap/interface/contrib/nav_test_framework/mininode.py
Executable file
943
basicswap/interface/contrib/nav_test_framework/script.py
Executable file
@@ -0,0 +1,943 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright (c) 2015-2016 The Bitcoin Core developers
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
|
#
|
||||||
|
# script.py
|
||||||
|
#
|
||||||
|
# This file is modified from python-navcoinlib.
|
||||||
|
#
|
||||||
|
|
||||||
|
"""Scripts
|
||||||
|
|
||||||
|
Functionality to build scripts, as well as SignatureHash().
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from .mininode import CTransaction, CTxOut, sha256, hash256, uint256_from_str, ser_uint256, ser_string
|
||||||
|
from binascii import hexlify
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
import sys
|
||||||
|
bchr = chr
|
||||||
|
bord = ord
|
||||||
|
if sys.version > '3':
|
||||||
|
long = int
|
||||||
|
bchr = lambda x: bytes([x])
|
||||||
|
bord = lambda x: x
|
||||||
|
|
||||||
|
import struct
|
||||||
|
|
||||||
|
from .bignum import bn2vch
|
||||||
|
|
||||||
|
MAX_SCRIPT_SIZE = 10000
|
||||||
|
MAX_SCRIPT_ELEMENT_SIZE = 520
|
||||||
|
MAX_SCRIPT_OPCODES = 201
|
||||||
|
|
||||||
|
OPCODE_NAMES = {}
|
||||||
|
|
||||||
|
_opcode_instances = []
|
||||||
|
class CScriptOp(int):
|
||||||
|
"""A single script opcode"""
|
||||||
|
__slots__ = []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def encode_op_pushdata(d):
|
||||||
|
"""Encode a PUSHDATA op, returning bytes"""
|
||||||
|
if len(d) < 0x4c:
|
||||||
|
return b'' + bchr(len(d)) + d # OP_PUSHDATA
|
||||||
|
elif len(d) <= 0xff:
|
||||||
|
return b'\x4c' + bchr(len(d)) + d # OP_PUSHDATA1
|
||||||
|
elif len(d) <= 0xffff:
|
||||||
|
return b'\x4d' + struct.pack(b'<H', len(d)) + d # OP_PUSHDATA2
|
||||||
|
elif len(d) <= 0xffffffff:
|
||||||
|
return b'\x4e' + struct.pack(b'<I', len(d)) + d # OP_PUSHDATA4
|
||||||
|
else:
|
||||||
|
raise ValueError("Data too long to encode in a PUSHDATA op")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def encode_op_n(n):
|
||||||
|
"""Encode a small integer op, returning an opcode"""
|
||||||
|
if not (0 <= n <= 16):
|
||||||
|
raise ValueError('Integer must be in range 0 <= n <= 16, got %d' % n)
|
||||||
|
|
||||||
|
if n == 0:
|
||||||
|
return OP_0
|
||||||
|
else:
|
||||||
|
return CScriptOp(OP_1 + n-1)
|
||||||
|
|
||||||
|
def decode_op_n(self):
|
||||||
|
"""Decode a small integer opcode, returning an integer"""
|
||||||
|
if self == OP_0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if not (self == OP_0 or OP_1 <= self <= OP_16):
|
||||||
|
raise ValueError('op %r is not an OP_N' % self)
|
||||||
|
|
||||||
|
return int(self - OP_1+1)
|
||||||
|
|
||||||
|
def is_small_int(self):
|
||||||
|
"""Return true if the op pushes a small integer to the stack"""
|
||||||
|
if 0x51 <= self <= 0x60 or self == 0:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return repr(self)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if self in OPCODE_NAMES:
|
||||||
|
return OPCODE_NAMES[self]
|
||||||
|
else:
|
||||||
|
return 'CScriptOp(0x%x)' % self
|
||||||
|
|
||||||
|
def __new__(cls, n):
|
||||||
|
try:
|
||||||
|
return _opcode_instances[n]
|
||||||
|
except IndexError:
|
||||||
|
assert len(_opcode_instances) == n
|
||||||
|
_opcode_instances.append(super(CScriptOp, cls).__new__(cls, n))
|
||||||
|
return _opcode_instances[n]
|
||||||
|
|
||||||
|
# Populate opcode instance table
|
||||||
|
for n in range(0xff+1):
|
||||||
|
CScriptOp(n)
|
||||||
|
|
||||||
|
|
||||||
|
# push value
|
||||||
|
OP_0 = CScriptOp(0x00)
|
||||||
|
OP_FALSE = OP_0
|
||||||
|
OP_PUSHDATA1 = CScriptOp(0x4c)
|
||||||
|
OP_PUSHDATA2 = CScriptOp(0x4d)
|
||||||
|
OP_PUSHDATA4 = CScriptOp(0x4e)
|
||||||
|
OP_1NEGATE = CScriptOp(0x4f)
|
||||||
|
OP_RESERVED = CScriptOp(0x50)
|
||||||
|
OP_1 = CScriptOp(0x51)
|
||||||
|
OP_TRUE=OP_1
|
||||||
|
OP_2 = CScriptOp(0x52)
|
||||||
|
OP_3 = CScriptOp(0x53)
|
||||||
|
OP_4 = CScriptOp(0x54)
|
||||||
|
OP_5 = CScriptOp(0x55)
|
||||||
|
OP_6 = CScriptOp(0x56)
|
||||||
|
OP_7 = CScriptOp(0x57)
|
||||||
|
OP_8 = CScriptOp(0x58)
|
||||||
|
OP_9 = CScriptOp(0x59)
|
||||||
|
OP_10 = CScriptOp(0x5a)
|
||||||
|
OP_11 = CScriptOp(0x5b)
|
||||||
|
OP_12 = CScriptOp(0x5c)
|
||||||
|
OP_13 = CScriptOp(0x5d)
|
||||||
|
OP_14 = CScriptOp(0x5e)
|
||||||
|
OP_15 = CScriptOp(0x5f)
|
||||||
|
OP_16 = CScriptOp(0x60)
|
||||||
|
|
||||||
|
# control
|
||||||
|
OP_NOP = CScriptOp(0x61)
|
||||||
|
OP_VER = CScriptOp(0x62)
|
||||||
|
OP_IF = CScriptOp(0x63)
|
||||||
|
OP_NOTIF = CScriptOp(0x64)
|
||||||
|
OP_VERIF = CScriptOp(0x65)
|
||||||
|
OP_VERNOTIF = CScriptOp(0x66)
|
||||||
|
OP_ELSE = CScriptOp(0x67)
|
||||||
|
OP_ENDIF = CScriptOp(0x68)
|
||||||
|
OP_VERIFY = CScriptOp(0x69)
|
||||||
|
OP_RETURN = CScriptOp(0x6a)
|
||||||
|
|
||||||
|
# stack ops
|
||||||
|
OP_TOALTSTACK = CScriptOp(0x6b)
|
||||||
|
OP_FROMALTSTACK = CScriptOp(0x6c)
|
||||||
|
OP_2DROP = CScriptOp(0x6d)
|
||||||
|
OP_2DUP = CScriptOp(0x6e)
|
||||||
|
OP_3DUP = CScriptOp(0x6f)
|
||||||
|
OP_2OVER = CScriptOp(0x70)
|
||||||
|
OP_2ROT = CScriptOp(0x71)
|
||||||
|
OP_2SWAP = CScriptOp(0x72)
|
||||||
|
OP_IFDUP = CScriptOp(0x73)
|
||||||
|
OP_DEPTH = CScriptOp(0x74)
|
||||||
|
OP_DROP = CScriptOp(0x75)
|
||||||
|
OP_DUP = CScriptOp(0x76)
|
||||||
|
OP_NIP = CScriptOp(0x77)
|
||||||
|
OP_OVER = CScriptOp(0x78)
|
||||||
|
OP_PICK = CScriptOp(0x79)
|
||||||
|
OP_ROLL = CScriptOp(0x7a)
|
||||||
|
OP_ROT = CScriptOp(0x7b)
|
||||||
|
OP_SWAP = CScriptOp(0x7c)
|
||||||
|
OP_TUCK = CScriptOp(0x7d)
|
||||||
|
|
||||||
|
# splice ops
|
||||||
|
OP_CAT = CScriptOp(0x7e)
|
||||||
|
OP_SUBSTR = CScriptOp(0x7f)
|
||||||
|
OP_LEFT = CScriptOp(0x80)
|
||||||
|
OP_RIGHT = CScriptOp(0x81)
|
||||||
|
OP_SIZE = CScriptOp(0x82)
|
||||||
|
|
||||||
|
# bit logic
|
||||||
|
OP_INVERT = CScriptOp(0x83)
|
||||||
|
OP_AND = CScriptOp(0x84)
|
||||||
|
OP_OR = CScriptOp(0x85)
|
||||||
|
OP_XOR = CScriptOp(0x86)
|
||||||
|
OP_EQUAL = CScriptOp(0x87)
|
||||||
|
OP_EQUALVERIFY = CScriptOp(0x88)
|
||||||
|
OP_RESERVED1 = CScriptOp(0x89)
|
||||||
|
OP_RESERVED2 = CScriptOp(0x8a)
|
||||||
|
|
||||||
|
# numeric
|
||||||
|
OP_1ADD = CScriptOp(0x8b)
|
||||||
|
OP_1SUB = CScriptOp(0x8c)
|
||||||
|
OP_2MUL = CScriptOp(0x8d)
|
||||||
|
OP_2DIV = CScriptOp(0x8e)
|
||||||
|
OP_NEGATE = CScriptOp(0x8f)
|
||||||
|
OP_ABS = CScriptOp(0x90)
|
||||||
|
OP_NOT = CScriptOp(0x91)
|
||||||
|
OP_0NOTEQUAL = CScriptOp(0x92)
|
||||||
|
|
||||||
|
OP_ADD = CScriptOp(0x93)
|
||||||
|
OP_SUB = CScriptOp(0x94)
|
||||||
|
OP_MUL = CScriptOp(0x95)
|
||||||
|
OP_DIV = CScriptOp(0x96)
|
||||||
|
OP_MOD = CScriptOp(0x97)
|
||||||
|
OP_LSHIFT = CScriptOp(0x98)
|
||||||
|
OP_RSHIFT = CScriptOp(0x99)
|
||||||
|
|
||||||
|
OP_BOOLAND = CScriptOp(0x9a)
|
||||||
|
OP_BOOLOR = CScriptOp(0x9b)
|
||||||
|
OP_NUMEQUAL = CScriptOp(0x9c)
|
||||||
|
OP_NUMEQUALVERIFY = CScriptOp(0x9d)
|
||||||
|
OP_NUMNOTEQUAL = CScriptOp(0x9e)
|
||||||
|
OP_LESSTHAN = CScriptOp(0x9f)
|
||||||
|
OP_GREATERTHAN = CScriptOp(0xa0)
|
||||||
|
OP_LESSTHANOREQUAL = CScriptOp(0xa1)
|
||||||
|
OP_GREATERTHANOREQUAL = CScriptOp(0xa2)
|
||||||
|
OP_MIN = CScriptOp(0xa3)
|
||||||
|
OP_MAX = CScriptOp(0xa4)
|
||||||
|
|
||||||
|
OP_WITHIN = CScriptOp(0xa5)
|
||||||
|
|
||||||
|
# crypto
|
||||||
|
OP_RIPEMD160 = CScriptOp(0xa6)
|
||||||
|
OP_SHA1 = CScriptOp(0xa7)
|
||||||
|
OP_SHA256 = CScriptOp(0xa8)
|
||||||
|
OP_HASH160 = CScriptOp(0xa9)
|
||||||
|
OP_HASH256 = CScriptOp(0xaa)
|
||||||
|
OP_CODESEPARATOR = CScriptOp(0xab)
|
||||||
|
OP_CHECKSIG = CScriptOp(0xac)
|
||||||
|
OP_CHECKSIGVERIFY = CScriptOp(0xad)
|
||||||
|
OP_CHECKMULTISIG = CScriptOp(0xae)
|
||||||
|
OP_CHECKMULTISIGVERIFY = CScriptOp(0xaf)
|
||||||
|
|
||||||
|
# expansion
|
||||||
|
OP_NOP1 = CScriptOp(0xb0)
|
||||||
|
OP_CHECKLOCKTIMEVERIFY = CScriptOp(0xb1)
|
||||||
|
OP_CHECKSEQUENCEVERIFY = CScriptOp(0xb2)
|
||||||
|
OP_NOP4 = CScriptOp(0xb3)
|
||||||
|
OP_NOP5 = CScriptOp(0xb4)
|
||||||
|
OP_NOP6 = CScriptOp(0xb5)
|
||||||
|
OP_NOP7 = CScriptOp(0xb6)
|
||||||
|
OP_NOP8 = CScriptOp(0xb7)
|
||||||
|
OP_NOP9 = CScriptOp(0xb8)
|
||||||
|
OP_NOP10 = CScriptOp(0xb9)
|
||||||
|
|
||||||
|
# template matching params
|
||||||
|
OP_SMALLINTEGER = CScriptOp(0xfa)
|
||||||
|
OP_PUBKEYS = CScriptOp(0xfb)
|
||||||
|
OP_PUBKEYHASH = CScriptOp(0xfd)
|
||||||
|
OP_PUBKEY = CScriptOp(0xfe)
|
||||||
|
|
||||||
|
OP_INVALIDOPCODE = CScriptOp(0xff)
|
||||||
|
|
||||||
|
VALID_OPCODES = {
|
||||||
|
OP_1NEGATE,
|
||||||
|
OP_RESERVED,
|
||||||
|
OP_1,
|
||||||
|
OP_2,
|
||||||
|
OP_3,
|
||||||
|
OP_4,
|
||||||
|
OP_5,
|
||||||
|
OP_6,
|
||||||
|
OP_7,
|
||||||
|
OP_8,
|
||||||
|
OP_9,
|
||||||
|
OP_10,
|
||||||
|
OP_11,
|
||||||
|
OP_12,
|
||||||
|
OP_13,
|
||||||
|
OP_14,
|
||||||
|
OP_15,
|
||||||
|
OP_16,
|
||||||
|
|
||||||
|
OP_NOP,
|
||||||
|
OP_VER,
|
||||||
|
OP_IF,
|
||||||
|
OP_NOTIF,
|
||||||
|
OP_VERIF,
|
||||||
|
OP_VERNOTIF,
|
||||||
|
OP_ELSE,
|
||||||
|
OP_ENDIF,
|
||||||
|
OP_VERIFY,
|
||||||
|
OP_RETURN,
|
||||||
|
|
||||||
|
OP_TOALTSTACK,
|
||||||
|
OP_FROMALTSTACK,
|
||||||
|
OP_2DROP,
|
||||||
|
OP_2DUP,
|
||||||
|
OP_3DUP,
|
||||||
|
OP_2OVER,
|
||||||
|
OP_2ROT,
|
||||||
|
OP_2SWAP,
|
||||||
|
OP_IFDUP,
|
||||||
|
OP_DEPTH,
|
||||||
|
OP_DROP,
|
||||||
|
OP_DUP,
|
||||||
|
OP_NIP,
|
||||||
|
OP_OVER,
|
||||||
|
OP_PICK,
|
||||||
|
OP_ROLL,
|
||||||
|
OP_ROT,
|
||||||
|
OP_SWAP,
|
||||||
|
OP_TUCK,
|
||||||
|
|
||||||
|
OP_CAT,
|
||||||
|
OP_SUBSTR,
|
||||||
|
OP_LEFT,
|
||||||
|
OP_RIGHT,
|
||||||
|
OP_SIZE,
|
||||||
|
|
||||||
|
OP_INVERT,
|
||||||
|
OP_AND,
|
||||||
|
OP_OR,
|
||||||
|
OP_XOR,
|
||||||
|
OP_EQUAL,
|
||||||
|
OP_EQUALVERIFY,
|
||||||
|
OP_RESERVED1,
|
||||||
|
OP_RESERVED2,
|
||||||
|
|
||||||
|
OP_1ADD,
|
||||||
|
OP_1SUB,
|
||||||
|
OP_2MUL,
|
||||||
|
OP_2DIV,
|
||||||
|
OP_NEGATE,
|
||||||
|
OP_ABS,
|
||||||
|
OP_NOT,
|
||||||
|
OP_0NOTEQUAL,
|
||||||
|
|
||||||
|
OP_ADD,
|
||||||
|
OP_SUB,
|
||||||
|
OP_MUL,
|
||||||
|
OP_DIV,
|
||||||
|
OP_MOD,
|
||||||
|
OP_LSHIFT,
|
||||||
|
OP_RSHIFT,
|
||||||
|
|
||||||
|
OP_BOOLAND,
|
||||||
|
OP_BOOLOR,
|
||||||
|
OP_NUMEQUAL,
|
||||||
|
OP_NUMEQUALVERIFY,
|
||||||
|
OP_NUMNOTEQUAL,
|
||||||
|
OP_LESSTHAN,
|
||||||
|
OP_GREATERTHAN,
|
||||||
|
OP_LESSTHANOREQUAL,
|
||||||
|
OP_GREATERTHANOREQUAL,
|
||||||
|
OP_MIN,
|
||||||
|
OP_MAX,
|
||||||
|
|
||||||
|
OP_WITHIN,
|
||||||
|
|
||||||
|
OP_RIPEMD160,
|
||||||
|
OP_SHA1,
|
||||||
|
OP_SHA256,
|
||||||
|
OP_HASH160,
|
||||||
|
OP_HASH256,
|
||||||
|
OP_CODESEPARATOR,
|
||||||
|
OP_CHECKSIG,
|
||||||
|
OP_CHECKSIGVERIFY,
|
||||||
|
OP_CHECKMULTISIG,
|
||||||
|
OP_CHECKMULTISIGVERIFY,
|
||||||
|
|
||||||
|
OP_NOP1,
|
||||||
|
OP_CHECKLOCKTIMEVERIFY,
|
||||||
|
OP_CHECKSEQUENCEVERIFY,
|
||||||
|
OP_NOP4,
|
||||||
|
OP_NOP5,
|
||||||
|
OP_NOP6,
|
||||||
|
OP_NOP7,
|
||||||
|
OP_NOP8,
|
||||||
|
OP_NOP9,
|
||||||
|
OP_NOP10,
|
||||||
|
|
||||||
|
OP_SMALLINTEGER,
|
||||||
|
OP_PUBKEYS,
|
||||||
|
OP_PUBKEYHASH,
|
||||||
|
OP_PUBKEY,
|
||||||
|
}
|
||||||
|
|
||||||
|
OPCODE_NAMES.update({
|
||||||
|
OP_0 : 'OP_0',
|
||||||
|
OP_PUSHDATA1 : 'OP_PUSHDATA1',
|
||||||
|
OP_PUSHDATA2 : 'OP_PUSHDATA2',
|
||||||
|
OP_PUSHDATA4 : 'OP_PUSHDATA4',
|
||||||
|
OP_1NEGATE : 'OP_1NEGATE',
|
||||||
|
OP_RESERVED : 'OP_RESERVED',
|
||||||
|
OP_1 : 'OP_1',
|
||||||
|
OP_2 : 'OP_2',
|
||||||
|
OP_3 : 'OP_3',
|
||||||
|
OP_4 : 'OP_4',
|
||||||
|
OP_5 : 'OP_5',
|
||||||
|
OP_6 : 'OP_6',
|
||||||
|
OP_7 : 'OP_7',
|
||||||
|
OP_8 : 'OP_8',
|
||||||
|
OP_9 : 'OP_9',
|
||||||
|
OP_10 : 'OP_10',
|
||||||
|
OP_11 : 'OP_11',
|
||||||
|
OP_12 : 'OP_12',
|
||||||
|
OP_13 : 'OP_13',
|
||||||
|
OP_14 : 'OP_14',
|
||||||
|
OP_15 : 'OP_15',
|
||||||
|
OP_16 : 'OP_16',
|
||||||
|
OP_NOP : 'OP_NOP',
|
||||||
|
OP_VER : 'OP_VER',
|
||||||
|
OP_IF : 'OP_IF',
|
||||||
|
OP_NOTIF : 'OP_NOTIF',
|
||||||
|
OP_VERIF : 'OP_VERIF',
|
||||||
|
OP_VERNOTIF : 'OP_VERNOTIF',
|
||||||
|
OP_ELSE : 'OP_ELSE',
|
||||||
|
OP_ENDIF : 'OP_ENDIF',
|
||||||
|
OP_VERIFY : 'OP_VERIFY',
|
||||||
|
OP_RETURN : 'OP_RETURN',
|
||||||
|
OP_TOALTSTACK : 'OP_TOALTSTACK',
|
||||||
|
OP_FROMALTSTACK : 'OP_FROMALTSTACK',
|
||||||
|
OP_2DROP : 'OP_2DROP',
|
||||||
|
OP_2DUP : 'OP_2DUP',
|
||||||
|
OP_3DUP : 'OP_3DUP',
|
||||||
|
OP_2OVER : 'OP_2OVER',
|
||||||
|
OP_2ROT : 'OP_2ROT',
|
||||||
|
OP_2SWAP : 'OP_2SWAP',
|
||||||
|
OP_IFDUP : 'OP_IFDUP',
|
||||||
|
OP_DEPTH : 'OP_DEPTH',
|
||||||
|
OP_DROP : 'OP_DROP',
|
||||||
|
OP_DUP : 'OP_DUP',
|
||||||
|
OP_NIP : 'OP_NIP',
|
||||||
|
OP_OVER : 'OP_OVER',
|
||||||
|
OP_PICK : 'OP_PICK',
|
||||||
|
OP_ROLL : 'OP_ROLL',
|
||||||
|
OP_ROT : 'OP_ROT',
|
||||||
|
OP_SWAP : 'OP_SWAP',
|
||||||
|
OP_TUCK : 'OP_TUCK',
|
||||||
|
OP_CAT : 'OP_CAT',
|
||||||
|
OP_SUBSTR : 'OP_SUBSTR',
|
||||||
|
OP_LEFT : 'OP_LEFT',
|
||||||
|
OP_RIGHT : 'OP_RIGHT',
|
||||||
|
OP_SIZE : 'OP_SIZE',
|
||||||
|
OP_INVERT : 'OP_INVERT',
|
||||||
|
OP_AND : 'OP_AND',
|
||||||
|
OP_OR : 'OP_OR',
|
||||||
|
OP_XOR : 'OP_XOR',
|
||||||
|
OP_EQUAL : 'OP_EQUAL',
|
||||||
|
OP_EQUALVERIFY : 'OP_EQUALVERIFY',
|
||||||
|
OP_RESERVED1 : 'OP_RESERVED1',
|
||||||
|
OP_RESERVED2 : 'OP_RESERVED2',
|
||||||
|
OP_1ADD : 'OP_1ADD',
|
||||||
|
OP_1SUB : 'OP_1SUB',
|
||||||
|
OP_2MUL : 'OP_2MUL',
|
||||||
|
OP_2DIV : 'OP_2DIV',
|
||||||
|
OP_NEGATE : 'OP_NEGATE',
|
||||||
|
OP_ABS : 'OP_ABS',
|
||||||
|
OP_NOT : 'OP_NOT',
|
||||||
|
OP_0NOTEQUAL : 'OP_0NOTEQUAL',
|
||||||
|
OP_ADD : 'OP_ADD',
|
||||||
|
OP_SUB : 'OP_SUB',
|
||||||
|
OP_MUL : 'OP_MUL',
|
||||||
|
OP_DIV : 'OP_DIV',
|
||||||
|
OP_MOD : 'OP_MOD',
|
||||||
|
OP_LSHIFT : 'OP_LSHIFT',
|
||||||
|
OP_RSHIFT : 'OP_RSHIFT',
|
||||||
|
OP_BOOLAND : 'OP_BOOLAND',
|
||||||
|
OP_BOOLOR : 'OP_BOOLOR',
|
||||||
|
OP_NUMEQUAL : 'OP_NUMEQUAL',
|
||||||
|
OP_NUMEQUALVERIFY : 'OP_NUMEQUALVERIFY',
|
||||||
|
OP_NUMNOTEQUAL : 'OP_NUMNOTEQUAL',
|
||||||
|
OP_LESSTHAN : 'OP_LESSTHAN',
|
||||||
|
OP_GREATERTHAN : 'OP_GREATERTHAN',
|
||||||
|
OP_LESSTHANOREQUAL : 'OP_LESSTHANOREQUAL',
|
||||||
|
OP_GREATERTHANOREQUAL : 'OP_GREATERTHANOREQUAL',
|
||||||
|
OP_MIN : 'OP_MIN',
|
||||||
|
OP_MAX : 'OP_MAX',
|
||||||
|
OP_WITHIN : 'OP_WITHIN',
|
||||||
|
OP_RIPEMD160 : 'OP_RIPEMD160',
|
||||||
|
OP_SHA1 : 'OP_SHA1',
|
||||||
|
OP_SHA256 : 'OP_SHA256',
|
||||||
|
OP_HASH160 : 'OP_HASH160',
|
||||||
|
OP_HASH256 : 'OP_HASH256',
|
||||||
|
OP_CODESEPARATOR : 'OP_CODESEPARATOR',
|
||||||
|
OP_CHECKSIG : 'OP_CHECKSIG',
|
||||||
|
OP_CHECKSIGVERIFY : 'OP_CHECKSIGVERIFY',
|
||||||
|
OP_CHECKMULTISIG : 'OP_CHECKMULTISIG',
|
||||||
|
OP_CHECKMULTISIGVERIFY : 'OP_CHECKMULTISIGVERIFY',
|
||||||
|
OP_NOP1 : 'OP_NOP1',
|
||||||
|
OP_CHECKLOCKTIMEVERIFY : 'OP_CHECKLOCKTIMEVERIFY',
|
||||||
|
OP_CHECKSEQUENCEVERIFY : 'OP_CHECKSEQUENCEVERIFY',
|
||||||
|
OP_NOP4 : 'OP_NOP4',
|
||||||
|
OP_NOP5 : 'OP_NOP5',
|
||||||
|
OP_NOP6 : 'OP_NOP6',
|
||||||
|
OP_NOP7 : 'OP_NOP7',
|
||||||
|
OP_NOP8 : 'OP_NOP8',
|
||||||
|
OP_NOP9 : 'OP_NOP9',
|
||||||
|
OP_NOP10 : 'OP_NOP10',
|
||||||
|
OP_SMALLINTEGER : 'OP_SMALLINTEGER',
|
||||||
|
OP_PUBKEYS : 'OP_PUBKEYS',
|
||||||
|
OP_PUBKEYHASH : 'OP_PUBKEYHASH',
|
||||||
|
OP_PUBKEY : 'OP_PUBKEY',
|
||||||
|
OP_INVALIDOPCODE : 'OP_INVALIDOPCODE',
|
||||||
|
})
|
||||||
|
|
||||||
|
OPCODES_BY_NAME = {
|
||||||
|
'OP_0' : OP_0,
|
||||||
|
'OP_PUSHDATA1' : OP_PUSHDATA1,
|
||||||
|
'OP_PUSHDATA2' : OP_PUSHDATA2,
|
||||||
|
'OP_PUSHDATA4' : OP_PUSHDATA4,
|
||||||
|
'OP_1NEGATE' : OP_1NEGATE,
|
||||||
|
'OP_RESERVED' : OP_RESERVED,
|
||||||
|
'OP_1' : OP_1,
|
||||||
|
'OP_2' : OP_2,
|
||||||
|
'OP_3' : OP_3,
|
||||||
|
'OP_4' : OP_4,
|
||||||
|
'OP_5' : OP_5,
|
||||||
|
'OP_6' : OP_6,
|
||||||
|
'OP_7' : OP_7,
|
||||||
|
'OP_8' : OP_8,
|
||||||
|
'OP_9' : OP_9,
|
||||||
|
'OP_10' : OP_10,
|
||||||
|
'OP_11' : OP_11,
|
||||||
|
'OP_12' : OP_12,
|
||||||
|
'OP_13' : OP_13,
|
||||||
|
'OP_14' : OP_14,
|
||||||
|
'OP_15' : OP_15,
|
||||||
|
'OP_16' : OP_16,
|
||||||
|
'OP_NOP' : OP_NOP,
|
||||||
|
'OP_VER' : OP_VER,
|
||||||
|
'OP_IF' : OP_IF,
|
||||||
|
'OP_NOTIF' : OP_NOTIF,
|
||||||
|
'OP_VERIF' : OP_VERIF,
|
||||||
|
'OP_VERNOTIF' : OP_VERNOTIF,
|
||||||
|
'OP_ELSE' : OP_ELSE,
|
||||||
|
'OP_ENDIF' : OP_ENDIF,
|
||||||
|
'OP_VERIFY' : OP_VERIFY,
|
||||||
|
'OP_RETURN' : OP_RETURN,
|
||||||
|
'OP_TOALTSTACK' : OP_TOALTSTACK,
|
||||||
|
'OP_FROMALTSTACK' : OP_FROMALTSTACK,
|
||||||
|
'OP_2DROP' : OP_2DROP,
|
||||||
|
'OP_2DUP' : OP_2DUP,
|
||||||
|
'OP_3DUP' : OP_3DUP,
|
||||||
|
'OP_2OVER' : OP_2OVER,
|
||||||
|
'OP_2ROT' : OP_2ROT,
|
||||||
|
'OP_2SWAP' : OP_2SWAP,
|
||||||
|
'OP_IFDUP' : OP_IFDUP,
|
||||||
|
'OP_DEPTH' : OP_DEPTH,
|
||||||
|
'OP_DROP' : OP_DROP,
|
||||||
|
'OP_DUP' : OP_DUP,
|
||||||
|
'OP_NIP' : OP_NIP,
|
||||||
|
'OP_OVER' : OP_OVER,
|
||||||
|
'OP_PICK' : OP_PICK,
|
||||||
|
'OP_ROLL' : OP_ROLL,
|
||||||
|
'OP_ROT' : OP_ROT,
|
||||||
|
'OP_SWAP' : OP_SWAP,
|
||||||
|
'OP_TUCK' : OP_TUCK,
|
||||||
|
'OP_CAT' : OP_CAT,
|
||||||
|
'OP_SUBSTR' : OP_SUBSTR,
|
||||||
|
'OP_LEFT' : OP_LEFT,
|
||||||
|
'OP_RIGHT' : OP_RIGHT,
|
||||||
|
'OP_SIZE' : OP_SIZE,
|
||||||
|
'OP_INVERT' : OP_INVERT,
|
||||||
|
'OP_AND' : OP_AND,
|
||||||
|
'OP_OR' : OP_OR,
|
||||||
|
'OP_XOR' : OP_XOR,
|
||||||
|
'OP_EQUAL' : OP_EQUAL,
|
||||||
|
'OP_EQUALVERIFY' : OP_EQUALVERIFY,
|
||||||
|
'OP_RESERVED1' : OP_RESERVED1,
|
||||||
|
'OP_RESERVED2' : OP_RESERVED2,
|
||||||
|
'OP_1ADD' : OP_1ADD,
|
||||||
|
'OP_1SUB' : OP_1SUB,
|
||||||
|
'OP_2MUL' : OP_2MUL,
|
||||||
|
'OP_2DIV' : OP_2DIV,
|
||||||
|
'OP_NEGATE' : OP_NEGATE,
|
||||||
|
'OP_ABS' : OP_ABS,
|
||||||
|
'OP_NOT' : OP_NOT,
|
||||||
|
'OP_0NOTEQUAL' : OP_0NOTEQUAL,
|
||||||
|
'OP_ADD' : OP_ADD,
|
||||||
|
'OP_SUB' : OP_SUB,
|
||||||
|
'OP_MUL' : OP_MUL,
|
||||||
|
'OP_DIV' : OP_DIV,
|
||||||
|
'OP_MOD' : OP_MOD,
|
||||||
|
'OP_LSHIFT' : OP_LSHIFT,
|
||||||
|
'OP_RSHIFT' : OP_RSHIFT,
|
||||||
|
'OP_BOOLAND' : OP_BOOLAND,
|
||||||
|
'OP_BOOLOR' : OP_BOOLOR,
|
||||||
|
'OP_NUMEQUAL' : OP_NUMEQUAL,
|
||||||
|
'OP_NUMEQUALVERIFY' : OP_NUMEQUALVERIFY,
|
||||||
|
'OP_NUMNOTEQUAL' : OP_NUMNOTEQUAL,
|
||||||
|
'OP_LESSTHAN' : OP_LESSTHAN,
|
||||||
|
'OP_GREATERTHAN' : OP_GREATERTHAN,
|
||||||
|
'OP_LESSTHANOREQUAL' : OP_LESSTHANOREQUAL,
|
||||||
|
'OP_GREATERTHANOREQUAL' : OP_GREATERTHANOREQUAL,
|
||||||
|
'OP_MIN' : OP_MIN,
|
||||||
|
'OP_MAX' : OP_MAX,
|
||||||
|
'OP_WITHIN' : OP_WITHIN,
|
||||||
|
'OP_RIPEMD160' : OP_RIPEMD160,
|
||||||
|
'OP_SHA1' : OP_SHA1,
|
||||||
|
'OP_SHA256' : OP_SHA256,
|
||||||
|
'OP_HASH160' : OP_HASH160,
|
||||||
|
'OP_HASH256' : OP_HASH256,
|
||||||
|
'OP_CODESEPARATOR' : OP_CODESEPARATOR,
|
||||||
|
'OP_CHECKSIG' : OP_CHECKSIG,
|
||||||
|
'OP_CHECKSIGVERIFY' : OP_CHECKSIGVERIFY,
|
||||||
|
'OP_CHECKMULTISIG' : OP_CHECKMULTISIG,
|
||||||
|
'OP_CHECKMULTISIGVERIFY' : OP_CHECKMULTISIGVERIFY,
|
||||||
|
'OP_NOP1' : OP_NOP1,
|
||||||
|
'OP_CHECKLOCKTIMEVERIFY' : OP_CHECKLOCKTIMEVERIFY,
|
||||||
|
'OP_CHECKSEQUENCEVERIFY' : OP_CHECKSEQUENCEVERIFY,
|
||||||
|
'OP_NOP4' : OP_NOP4,
|
||||||
|
'OP_NOP5' : OP_NOP5,
|
||||||
|
'OP_NOP6' : OP_NOP6,
|
||||||
|
'OP_NOP7' : OP_NOP7,
|
||||||
|
'OP_NOP8' : OP_NOP8,
|
||||||
|
'OP_NOP9' : OP_NOP9,
|
||||||
|
'OP_NOP10' : OP_NOP10,
|
||||||
|
'OP_SMALLINTEGER' : OP_SMALLINTEGER,
|
||||||
|
'OP_PUBKEYS' : OP_PUBKEYS,
|
||||||
|
'OP_PUBKEYHASH' : OP_PUBKEYHASH,
|
||||||
|
'OP_PUBKEY' : OP_PUBKEY,
|
||||||
|
}
|
||||||
|
|
||||||
|
class CScriptInvalidError(Exception):
|
||||||
|
"""Base class for CScript exceptions"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class CScriptTruncatedPushDataError(CScriptInvalidError):
|
||||||
|
"""Invalid pushdata due to truncation"""
|
||||||
|
def __init__(self, msg, data):
|
||||||
|
self.data = data
|
||||||
|
super(CScriptTruncatedPushDataError, self).__init__(msg)
|
||||||
|
|
||||||
|
# This is used, eg, for blockchain heights in coinbase scripts (bip34)
|
||||||
|
class CScriptNum(object):
|
||||||
|
def __init__(self, d=0):
|
||||||
|
self.value = d
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def encode(obj):
|
||||||
|
r = bytearray(0)
|
||||||
|
if obj.value == 0:
|
||||||
|
return bytes(r)
|
||||||
|
neg = obj.value < 0
|
||||||
|
absvalue = -obj.value if neg else obj.value
|
||||||
|
while (absvalue):
|
||||||
|
r.append(absvalue & 0xff)
|
||||||
|
absvalue >>= 8
|
||||||
|
if r[-1] & 0x80:
|
||||||
|
r.append(0x80 if neg else 0)
|
||||||
|
elif neg:
|
||||||
|
r[-1] |= 0x80
|
||||||
|
return bytes(bchr(len(r)) + r)
|
||||||
|
|
||||||
|
|
||||||
|
class CScript(bytes):
|
||||||
|
"""Serialized script
|
||||||
|
|
||||||
|
A bytes subclass, so you can use this directly whenever bytes are accepted.
|
||||||
|
Note that this means that indexing does *not* work - you'll get an index by
|
||||||
|
byte rather than opcode. This format was chosen for efficiency so that the
|
||||||
|
general case would not require creating a lot of little CScriptOP objects.
|
||||||
|
|
||||||
|
iter(script) however does iterate by opcode.
|
||||||
|
"""
|
||||||
|
@classmethod
|
||||||
|
def __coerce_instance(cls, other):
|
||||||
|
# Coerce other into bytes
|
||||||
|
if isinstance(other, CScriptOp):
|
||||||
|
other = bchr(other)
|
||||||
|
elif isinstance(other, CScriptNum):
|
||||||
|
if (other.value == 0):
|
||||||
|
other = bchr(CScriptOp(OP_0))
|
||||||
|
else:
|
||||||
|
other = CScriptNum.encode(other)
|
||||||
|
elif isinstance(other, int):
|
||||||
|
if 0 <= other <= 16:
|
||||||
|
other = bytes(bchr(CScriptOp.encode_op_n(other)))
|
||||||
|
elif other == -1:
|
||||||
|
other = bytes(bchr(OP_1NEGATE))
|
||||||
|
else:
|
||||||
|
other = CScriptOp.encode_op_pushdata(bn2vch(other))
|
||||||
|
elif isinstance(other, (bytes, bytearray)):
|
||||||
|
other = CScriptOp.encode_op_pushdata(other)
|
||||||
|
return other
|
||||||
|
|
||||||
|
def __add__(self, other):
|
||||||
|
# Do the coercion outside of the try block so that errors in it are
|
||||||
|
# noticed.
|
||||||
|
other = self.__coerce_instance(other)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# bytes.__add__ always returns bytes instances unfortunately
|
||||||
|
return CScript(super(CScript, self).__add__(other))
|
||||||
|
except TypeError:
|
||||||
|
raise TypeError('Can not add a %r instance to a CScript' % other.__class__)
|
||||||
|
|
||||||
|
def join(self, iterable):
|
||||||
|
# join makes no sense for a CScript()
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def __new__(cls, value=b''):
|
||||||
|
if isinstance(value, bytes) or isinstance(value, bytearray):
|
||||||
|
return super(CScript, cls).__new__(cls, value)
|
||||||
|
else:
|
||||||
|
def coerce_iterable(iterable):
|
||||||
|
for instance in iterable:
|
||||||
|
yield cls.__coerce_instance(instance)
|
||||||
|
# Annoyingly on both python2 and python3 bytes.join() always
|
||||||
|
# returns a bytes instance even when subclassed.
|
||||||
|
return super(CScript, cls).__new__(cls, b''.join(coerce_iterable(value)))
|
||||||
|
|
||||||
|
def raw_iter(self):
|
||||||
|
"""Raw iteration
|
||||||
|
|
||||||
|
Yields tuples of (opcode, data, sop_idx) so that the different possible
|
||||||
|
PUSHDATA encodings can be accurately distinguished, as well as
|
||||||
|
determining the exact opcode byte indexes. (sop_idx)
|
||||||
|
"""
|
||||||
|
i = 0
|
||||||
|
while i < len(self):
|
||||||
|
sop_idx = i
|
||||||
|
opcode = bord(self[i])
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if opcode > OP_PUSHDATA4:
|
||||||
|
yield (opcode, None, sop_idx)
|
||||||
|
else:
|
||||||
|
datasize = None
|
||||||
|
pushdata_type = None
|
||||||
|
if opcode < OP_PUSHDATA1:
|
||||||
|
pushdata_type = 'PUSHDATA(%d)' % opcode
|
||||||
|
datasize = opcode
|
||||||
|
|
||||||
|
elif opcode == OP_PUSHDATA1:
|
||||||
|
pushdata_type = 'PUSHDATA1'
|
||||||
|
if i >= len(self):
|
||||||
|
raise CScriptInvalidError('PUSHDATA1: missing data length')
|
||||||
|
datasize = bord(self[i])
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
elif opcode == OP_PUSHDATA2:
|
||||||
|
pushdata_type = 'PUSHDATA2'
|
||||||
|
if i + 1 >= len(self):
|
||||||
|
raise CScriptInvalidError('PUSHDATA2: missing data length')
|
||||||
|
datasize = bord(self[i]) + (bord(self[i+1]) << 8)
|
||||||
|
i += 2
|
||||||
|
|
||||||
|
elif opcode == OP_PUSHDATA4:
|
||||||
|
pushdata_type = 'PUSHDATA4'
|
||||||
|
if i + 3 >= len(self):
|
||||||
|
raise CScriptInvalidError('PUSHDATA4: missing data length')
|
||||||
|
datasize = bord(self[i]) + (bord(self[i+1]) << 8) + (bord(self[i+2]) << 16) + (bord(self[i+3]) << 24)
|
||||||
|
i += 4
|
||||||
|
|
||||||
|
else:
|
||||||
|
assert False # shouldn't happen
|
||||||
|
|
||||||
|
|
||||||
|
data = bytes(self[i:i+datasize])
|
||||||
|
|
||||||
|
# Check for truncation
|
||||||
|
if len(data) < datasize:
|
||||||
|
raise CScriptTruncatedPushDataError('%s: truncated data' % pushdata_type, data)
|
||||||
|
|
||||||
|
i += datasize
|
||||||
|
|
||||||
|
yield (opcode, data, sop_idx)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
"""'Cooked' iteration
|
||||||
|
|
||||||
|
Returns either a CScriptOP instance, an integer, or bytes, as
|
||||||
|
appropriate.
|
||||||
|
|
||||||
|
See raw_iter() if you need to distinguish the different possible
|
||||||
|
PUSHDATA encodings.
|
||||||
|
"""
|
||||||
|
for (opcode, data, sop_idx) in self.raw_iter():
|
||||||
|
if data is not None:
|
||||||
|
yield data
|
||||||
|
else:
|
||||||
|
opcode = CScriptOp(opcode)
|
||||||
|
|
||||||
|
if opcode.is_small_int():
|
||||||
|
yield opcode.decode_op_n()
|
||||||
|
else:
|
||||||
|
yield CScriptOp(opcode)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
# For Python3 compatibility add b before strings so testcases don't
|
||||||
|
# need to change
|
||||||
|
def _repr(o):
|
||||||
|
if isinstance(o, bytes):
|
||||||
|
return b"x('%s')" % hexlify(o).decode('ascii')
|
||||||
|
else:
|
||||||
|
return repr(o)
|
||||||
|
|
||||||
|
ops = []
|
||||||
|
i = iter(self)
|
||||||
|
while True:
|
||||||
|
op = None
|
||||||
|
try:
|
||||||
|
op = _repr(next(i))
|
||||||
|
except CScriptTruncatedPushDataError as err:
|
||||||
|
op = '%s...<ERROR: %s>' % (_repr(err.data), err)
|
||||||
|
break
|
||||||
|
except CScriptInvalidError as err:
|
||||||
|
op = '<ERROR: %s>' % err
|
||||||
|
break
|
||||||
|
except StopIteration:
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
if op is not None:
|
||||||
|
ops.append(op)
|
||||||
|
|
||||||
|
return "CScript([%s])" % ', '.join(ops)
|
||||||
|
|
||||||
|
def GetSigOpCount(self, fAccurate):
|
||||||
|
"""Get the SigOp count.
|
||||||
|
|
||||||
|
fAccurate - Accurately count CHECKMULTISIG, see BIP16 for details.
|
||||||
|
|
||||||
|
Note that this is consensus-critical.
|
||||||
|
"""
|
||||||
|
n = 0
|
||||||
|
lastOpcode = OP_INVALIDOPCODE
|
||||||
|
for (opcode, data, sop_idx) in self.raw_iter():
|
||||||
|
if opcode in (OP_CHECKSIG, OP_CHECKSIGVERIFY):
|
||||||
|
n += 1
|
||||||
|
elif opcode in (OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY):
|
||||||
|
if fAccurate and (OP_1 <= lastOpcode <= OP_16):
|
||||||
|
n += opcode.decode_op_n()
|
||||||
|
else:
|
||||||
|
n += 20
|
||||||
|
lastOpcode = opcode
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
SIGHASH_ALL = 1
|
||||||
|
SIGHASH_NONE = 2
|
||||||
|
SIGHASH_SINGLE = 3
|
||||||
|
SIGHASH_ANYONECANPAY = 0x80
|
||||||
|
|
||||||
|
def FindAndDelete(script, sig):
|
||||||
|
"""Consensus critical, see FindAndDelete() in Satoshi codebase"""
|
||||||
|
r = b''
|
||||||
|
last_sop_idx = sop_idx = 0
|
||||||
|
skip = True
|
||||||
|
for (opcode, data, sop_idx) in script.raw_iter():
|
||||||
|
if not skip:
|
||||||
|
r += script[last_sop_idx:sop_idx]
|
||||||
|
last_sop_idx = sop_idx
|
||||||
|
if script[sop_idx:sop_idx + len(sig)] == sig:
|
||||||
|
skip = True
|
||||||
|
else:
|
||||||
|
skip = False
|
||||||
|
if not skip:
|
||||||
|
r += script[last_sop_idx:]
|
||||||
|
return CScript(r)
|
||||||
|
|
||||||
|
|
||||||
|
def SignatureHash(script, txTo, inIdx, hashtype):
|
||||||
|
"""Consensus-correct SignatureHash
|
||||||
|
|
||||||
|
Returns (hash, err) to precisely match the consensus-critical behavior of
|
||||||
|
the SIGHASH_SINGLE bug. (inIdx is *not* checked for validity)
|
||||||
|
"""
|
||||||
|
HASH_ONE = b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
|
||||||
|
if inIdx >= len(txTo.vin):
|
||||||
|
return (HASH_ONE, "inIdx %d out of range (%d)" % (inIdx, len(txTo.vin)))
|
||||||
|
txtmp = CTransaction(txTo)
|
||||||
|
|
||||||
|
for txin in txtmp.vin:
|
||||||
|
txin.scriptSig = b''
|
||||||
|
txtmp.vin[inIdx].scriptSig = FindAndDelete(script, CScript([OP_CODESEPARATOR]))
|
||||||
|
|
||||||
|
if (hashtype & 0x1f) == SIGHASH_NONE:
|
||||||
|
txtmp.vout = []
|
||||||
|
|
||||||
|
for i in range(len(txtmp.vin)):
|
||||||
|
if i != inIdx:
|
||||||
|
txtmp.vin[i].nSequence = 0
|
||||||
|
|
||||||
|
elif (hashtype & 0x1f) == SIGHASH_SINGLE:
|
||||||
|
outIdx = inIdx
|
||||||
|
if outIdx >= len(txtmp.vout):
|
||||||
|
return (HASH_ONE, "outIdx %d out of range (%d)" % (outIdx, len(txtmp.vout)))
|
||||||
|
|
||||||
|
tmp = txtmp.vout[outIdx]
|
||||||
|
txtmp.vout = []
|
||||||
|
for i in range(outIdx):
|
||||||
|
txtmp.vout.append(CTxOut())
|
||||||
|
txtmp.vout.append(tmp)
|
||||||
|
|
||||||
|
for i in range(len(txtmp.vin)):
|
||||||
|
if i != inIdx:
|
||||||
|
txtmp.vin[i].nSequence = 0
|
||||||
|
|
||||||
|
if hashtype & SIGHASH_ANYONECANPAY:
|
||||||
|
tmp = txtmp.vin[inIdx]
|
||||||
|
txtmp.vin = []
|
||||||
|
txtmp.vin.append(tmp)
|
||||||
|
|
||||||
|
s = txtmp.serialize()
|
||||||
|
s += struct.pack(b"<I", hashtype)
|
||||||
|
|
||||||
|
hash = hash256(s)
|
||||||
|
|
||||||
|
return (hash, None)
|
||||||
|
|
||||||
|
# TODO: Allow cached hashPrevouts/hashSequence/hashOutputs to be provided.
|
||||||
|
# Performance optimization probably not necessary for python tests, however.
|
||||||
|
# Note that this corresponds to sigversion == 1 in EvalScript, which is used
|
||||||
|
# for version 0 witnesses.
|
||||||
|
def SegwitVersion1SignatureHash(script, txTo, inIdx, hashtype, amount):
|
||||||
|
|
||||||
|
hashPrevouts = 0
|
||||||
|
hashSequence = 0
|
||||||
|
hashOutputs = 0
|
||||||
|
|
||||||
|
if not (hashtype & SIGHASH_ANYONECANPAY):
|
||||||
|
serialize_prevouts = bytes()
|
||||||
|
for i in txTo.vin:
|
||||||
|
serialize_prevouts += i.prevout.serialize()
|
||||||
|
hashPrevouts = uint256_from_str(hash256(serialize_prevouts))
|
||||||
|
|
||||||
|
if (not (hashtype & SIGHASH_ANYONECANPAY) and (hashtype & 0x1f) != SIGHASH_SINGLE and (hashtype & 0x1f) != SIGHASH_NONE):
|
||||||
|
serialize_sequence = bytes()
|
||||||
|
for i in txTo.vin:
|
||||||
|
serialize_sequence += struct.pack("<I", i.nSequence)
|
||||||
|
hashSequence = uint256_from_str(hash256(serialize_sequence))
|
||||||
|
|
||||||
|
if ((hashtype & 0x1f) != SIGHASH_SINGLE and (hashtype & 0x1f) != SIGHASH_NONE):
|
||||||
|
serialize_outputs = bytes()
|
||||||
|
for o in txTo.vout:
|
||||||
|
serialize_outputs += o.serialize()
|
||||||
|
hashOutputs = uint256_from_str(hash256(serialize_outputs))
|
||||||
|
elif ((hashtype & 0x1f) == SIGHASH_SINGLE and inIdx < len(txTo.vout)):
|
||||||
|
serialize_outputs = txTo.vout[inIdx].serialize()
|
||||||
|
hashOutputs = uint256_from_str(hash256(serialize_outputs))
|
||||||
|
|
||||||
|
ss = bytes()
|
||||||
|
ss += struct.pack("<i", txTo.nVersion)
|
||||||
|
ss += ser_uint256(hashPrevouts)
|
||||||
|
ss += ser_uint256(hashSequence)
|
||||||
|
ss += txTo.vin[inIdx].prevout.serialize()
|
||||||
|
ss += ser_string(script)
|
||||||
|
ss += struct.pack("<q", amount)
|
||||||
|
ss += struct.pack("<I", txTo.vin[inIdx].nSequence)
|
||||||
|
ss += ser_uint256(hashOutputs)
|
||||||
|
ss += struct.pack("<i", txTo.nLockTime)
|
||||||
|
ss += struct.pack("<I", hashtype)
|
||||||
|
|
||||||
|
return hash256(ss)
|
||||||
63
basicswap/interface/contrib/nav_test_framework/siphash.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright (c) 2016-2018 The Bitcoin Core developers
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
"""Specialized SipHash-2-4 implementations.
|
||||||
|
|
||||||
|
This implements SipHash-2-4 for 256-bit integers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def rotl64(n, b):
|
||||||
|
return n >> (64 - b) | (n & ((1 << (64 - b)) - 1)) << b
|
||||||
|
|
||||||
|
def siphash_round(v0, v1, v2, v3):
|
||||||
|
v0 = (v0 + v1) & ((1 << 64) - 1)
|
||||||
|
v1 = rotl64(v1, 13)
|
||||||
|
v1 ^= v0
|
||||||
|
v0 = rotl64(v0, 32)
|
||||||
|
v2 = (v2 + v3) & ((1 << 64) - 1)
|
||||||
|
v3 = rotl64(v3, 16)
|
||||||
|
v3 ^= v2
|
||||||
|
v0 = (v0 + v3) & ((1 << 64) - 1)
|
||||||
|
v3 = rotl64(v3, 21)
|
||||||
|
v3 ^= v0
|
||||||
|
v2 = (v2 + v1) & ((1 << 64) - 1)
|
||||||
|
v1 = rotl64(v1, 17)
|
||||||
|
v1 ^= v2
|
||||||
|
v2 = rotl64(v2, 32)
|
||||||
|
return (v0, v1, v2, v3)
|
||||||
|
|
||||||
|
def siphash256(k0, k1, h):
|
||||||
|
n0 = h & ((1 << 64) - 1)
|
||||||
|
n1 = (h >> 64) & ((1 << 64) - 1)
|
||||||
|
n2 = (h >> 128) & ((1 << 64) - 1)
|
||||||
|
n3 = (h >> 192) & ((1 << 64) - 1)
|
||||||
|
v0 = 0x736f6d6570736575 ^ k0
|
||||||
|
v1 = 0x646f72616e646f6d ^ k1
|
||||||
|
v2 = 0x6c7967656e657261 ^ k0
|
||||||
|
v3 = 0x7465646279746573 ^ k1 ^ n0
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0 ^= n0
|
||||||
|
v3 ^= n1
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0 ^= n1
|
||||||
|
v3 ^= n2
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0 ^= n2
|
||||||
|
v3 ^= n3
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0 ^= n3
|
||||||
|
v3 ^= 0x2000000000000000
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0 ^= 0x2000000000000000
|
||||||
|
v2 ^= 0xFF
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
return v0 ^ v1 ^ v2 ^ v3
|
||||||
700
basicswap/interface/contrib/nav_test_framework/util.py
Executable file
@@ -0,0 +1,700 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright (c) 2014-2016 The Bitcoin Core developers
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Helpful routines for regression testing
|
||||||
|
#
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from binascii import hexlify, unhexlify
|
||||||
|
from base64 import b64encode
|
||||||
|
from decimal import Decimal, ROUND_DOWN
|
||||||
|
import json
|
||||||
|
import http.client
|
||||||
|
import random
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
import errno
|
||||||
|
|
||||||
|
from . import coverage
|
||||||
|
from .authproxy import AuthServiceProxy, JSONRPCException
|
||||||
|
|
||||||
|
COVERAGE_DIR = None
|
||||||
|
|
||||||
|
# The maximum number of nodes a single test can spawn
|
||||||
|
MAX_NODES = 8
|
||||||
|
# Don't assign rpc or p2p ports lower than this
|
||||||
|
PORT_MIN = 11000
|
||||||
|
# The number of ports to "reserve" for p2p and rpc, each
|
||||||
|
PORT_RANGE = 5000
|
||||||
|
|
||||||
|
NAVCOIND_PROC_WAIT_TIMEOUT = 60
|
||||||
|
|
||||||
|
|
||||||
|
class PortSeed:
|
||||||
|
# Must be initialized with a unique integer for each process
|
||||||
|
n = None
|
||||||
|
|
||||||
|
#Set Mocktime default to OFF.
|
||||||
|
#MOCKTIME is only needed for scripts that use the
|
||||||
|
#cached version of the blockchain. If the cached
|
||||||
|
#version of the blockchain is used without MOCKTIME
|
||||||
|
#then the mempools will not sync due to IBD.
|
||||||
|
MOCKTIME = 0
|
||||||
|
|
||||||
|
def enable_mocktime():
|
||||||
|
#For backwared compatibility of the python scripts
|
||||||
|
#with previous versions of the cache, set MOCKTIME
|
||||||
|
#to Jan 1, 2014 + (201 * 10 * 60)
|
||||||
|
global MOCKTIME
|
||||||
|
MOCKTIME = 1388534400 + (201 * 10 * 60)
|
||||||
|
|
||||||
|
def disable_mocktime():
|
||||||
|
global MOCKTIME
|
||||||
|
MOCKTIME = 0
|
||||||
|
|
||||||
|
def get_mocktime():
|
||||||
|
return MOCKTIME
|
||||||
|
|
||||||
|
def enable_coverage(dirname):
|
||||||
|
"""Maintain a log of which RPC calls are made during testing."""
|
||||||
|
global COVERAGE_DIR
|
||||||
|
COVERAGE_DIR = dirname
|
||||||
|
|
||||||
|
def get_rpc_proxy(url, node_number, timeout=None):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
url (str): URL of the RPC server to call
|
||||||
|
node_number (int): the node number (or id) that this calls to
|
||||||
|
|
||||||
|
Kwargs:
|
||||||
|
timeout (int): HTTP timeout in seconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
AuthServiceProxy. convenience object for making RPC calls.
|
||||||
|
|
||||||
|
"""
|
||||||
|
proxy_kwargs = {}
|
||||||
|
if timeout is not None:
|
||||||
|
proxy_kwargs['timeout'] = timeout
|
||||||
|
|
||||||
|
proxy = AuthServiceProxy(url, **proxy_kwargs)
|
||||||
|
proxy.url = url # store URL on proxy for info
|
||||||
|
|
||||||
|
coverage_logfile = coverage.get_filename(
|
||||||
|
COVERAGE_DIR, node_number) if COVERAGE_DIR else None
|
||||||
|
|
||||||
|
return coverage.AuthServiceProxyWrapper(proxy, coverage_logfile)
|
||||||
|
|
||||||
|
|
||||||
|
def p2p_port(n):
|
||||||
|
assert(n <= MAX_NODES)
|
||||||
|
return PORT_MIN + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES)
|
||||||
|
|
||||||
|
def rpc_port(n):
|
||||||
|
return PORT_MIN + PORT_RANGE + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES)
|
||||||
|
|
||||||
|
def check_json_precision():
|
||||||
|
"""Make sure json library being used does not lose precision converting NAV values"""
|
||||||
|
n = Decimal("20000000.00000003")
|
||||||
|
satoshis = int(json.loads(json.dumps(float(n)))*1.0e8)
|
||||||
|
if satoshis != 2000000000000003:
|
||||||
|
raise RuntimeError("JSON encode/decode loses precision")
|
||||||
|
|
||||||
|
def count_bytes(hex_string):
|
||||||
|
return len(bytearray.fromhex(hex_string))
|
||||||
|
|
||||||
|
def bytes_to_hex_str(byte_str):
|
||||||
|
return hexlify(byte_str).decode('ascii')
|
||||||
|
|
||||||
|
def hex_str_to_bytes(hex_str):
|
||||||
|
return unhexlify(hex_str.encode('ascii'))
|
||||||
|
|
||||||
|
def str_to_b64str(string):
|
||||||
|
return b64encode(string.encode('utf-8')).decode('ascii')
|
||||||
|
|
||||||
|
def sync_blocks(rpc_connections, wait=1, timeout=60):
|
||||||
|
"""
|
||||||
|
Wait until everybody has the same tip
|
||||||
|
"""
|
||||||
|
while timeout > 0:
|
||||||
|
tips = [ x.getbestblockhash() for x in rpc_connections ]
|
||||||
|
if tips == [ tips[0] ]*len(tips):
|
||||||
|
#if all x.getblockhash() in tips are the same return True
|
||||||
|
return True
|
||||||
|
time.sleep(wait)
|
||||||
|
timeout -= wait
|
||||||
|
raise AssertionError("Block sync failed")
|
||||||
|
|
||||||
|
def sync_mempools(rpc_connections, wait=1, timeout=60):
|
||||||
|
"""
|
||||||
|
Wait until everybody has the same transactions in their memory
|
||||||
|
pools
|
||||||
|
"""
|
||||||
|
while timeout > 0:
|
||||||
|
pool = set(rpc_connections[0].getrawmempool())
|
||||||
|
num_match = 1
|
||||||
|
for i in range(1, len(rpc_connections)):
|
||||||
|
if set(rpc_connections[i].getrawmempool()) == pool:
|
||||||
|
num_match = num_match+1
|
||||||
|
if num_match == len(rpc_connections):
|
||||||
|
return True
|
||||||
|
time.sleep(wait)
|
||||||
|
timeout -= wait
|
||||||
|
raise AssertionError("Mempool sync failed")
|
||||||
|
|
||||||
|
navcoind_processes = {}
|
||||||
|
|
||||||
|
def initialize_datadir(dirname, n):
|
||||||
|
datadir = os.path.join(dirname, "node"+str(n))
|
||||||
|
if not os.path.isdir(datadir):
|
||||||
|
os.makedirs(datadir)
|
||||||
|
rpc_u, rpc_p = rpc_auth_pair(n)
|
||||||
|
with open(os.path.join(datadir, "navcoin.conf"), 'w') as f:
|
||||||
|
f.write("devnet=1\n")
|
||||||
|
f.write("rpcuser=" + rpc_u + "\n")
|
||||||
|
f.write("rpcpassword=" + rpc_p + "\n")
|
||||||
|
f.write("port="+str(p2p_port(n))+"\n")
|
||||||
|
f.write("rpcport="+str(rpc_port(n))+"\n")
|
||||||
|
f.write("listenonion=0\n")
|
||||||
|
f.write("dandelion=0\n")
|
||||||
|
f.write("ntpminmeasures=-1\n")
|
||||||
|
f.write("torserver=0\n")
|
||||||
|
f.write("suppressblsctwarning=1\n")
|
||||||
|
return datadir
|
||||||
|
|
||||||
|
def rpc_auth_pair(n):
|
||||||
|
return 'rpcuser💻' + str(n), 'rpcpass🔑' + str(n)
|
||||||
|
|
||||||
|
def rpc_url(i, rpchost=None):
|
||||||
|
rpc_u, rpc_p = rpc_auth_pair(i)
|
||||||
|
return "http://%s:%s@%s:%d" % (rpc_u, rpc_p, rpchost or '127.0.0.1', rpc_port(i))
|
||||||
|
|
||||||
|
def wait_for_navcoind_start(process, url, i):
|
||||||
|
'''
|
||||||
|
Wait for navcoind to start. This means that RPC is accessible and fully initialized.
|
||||||
|
Raise an exception if navcoind exits during initialization.
|
||||||
|
'''
|
||||||
|
polls_interval = 1.0 / 4
|
||||||
|
runtime = 60
|
||||||
|
while runtime > 0:
|
||||||
|
if process.poll() is not None:
|
||||||
|
raise Exception('navcoind exited with status %i during initialization' % process.returncode)
|
||||||
|
try:
|
||||||
|
# print('Checking RPC')
|
||||||
|
rpc = get_rpc_proxy(url, i)
|
||||||
|
blocks = rpc.getblockcount()
|
||||||
|
# print('RPC replied with blocks: %i' % blocks)
|
||||||
|
return # break out of loop on success
|
||||||
|
except IOError as e:
|
||||||
|
if e.errno != errno.ECONNREFUSED: # Port not yet open?
|
||||||
|
raise # unknown IO error
|
||||||
|
# else:
|
||||||
|
# print('Waiting for port')
|
||||||
|
except JSONRPCException as e: # Initialization phase
|
||||||
|
if e.error['code'] != -28: # RPC in warmup?
|
||||||
|
raise # unkown JSON RPC exception
|
||||||
|
# else:
|
||||||
|
# print('RPC in warmup')
|
||||||
|
time.sleep(polls_interval)
|
||||||
|
runtime -= polls_interval
|
||||||
|
raise Exception('navcoind RPC timeout')
|
||||||
|
|
||||||
|
def initialize_chain(test_dir, num_nodes):
|
||||||
|
"""
|
||||||
|
Create a cache of a 200-block-long chain (with wallet) for MAX_NODES
|
||||||
|
Afterward, create num_nodes copies from the cache
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert num_nodes <= MAX_NODES
|
||||||
|
create_cache = False
|
||||||
|
for i in range(MAX_NODES):
|
||||||
|
if not os.path.isdir(os.path.join('cache', 'node'+str(i))):
|
||||||
|
create_cache = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if create_cache:
|
||||||
|
|
||||||
|
#find and delete old cache directories if any exist
|
||||||
|
for i in range(MAX_NODES):
|
||||||
|
if os.path.isdir(os.path.join("cache","node"+str(i))):
|
||||||
|
shutil.rmtree(os.path.join("cache","node"+str(i)))
|
||||||
|
|
||||||
|
# Create cache directories, run navcoinds:
|
||||||
|
for i in range(MAX_NODES):
|
||||||
|
datadir=initialize_datadir("cache", i)
|
||||||
|
args = [ os.getenv("NAVCOIND", "navcoind"), "-server", "-keypool=1", "-datadir="+datadir, "-discover=0" ]
|
||||||
|
if i > 0:
|
||||||
|
args.append("-connect=127.0.0.1:"+str(p2p_port(0)))
|
||||||
|
navcoind_processes[i] = subprocess.Popen(args)
|
||||||
|
if os.getenv("PYTHON_DEBUG", ""):
|
||||||
|
print("initialize_chain: navcoind started, waiting for RPC to come up")
|
||||||
|
wait_for_navcoind_start(navcoind_processes[i], rpc_url(i), i)
|
||||||
|
if os.getenv("PYTHON_DEBUG", ""):
|
||||||
|
print("initialize_chain: RPC succesfully started")
|
||||||
|
|
||||||
|
rpcs = []
|
||||||
|
for i in range(MAX_NODES):
|
||||||
|
try:
|
||||||
|
rpcs.append(get_rpc_proxy(rpc_url(i), i))
|
||||||
|
except:
|
||||||
|
sys.stderr.write("Error connecting to "+url+"\n")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Create a 200-block-long chain; each of the 4 first nodes
|
||||||
|
# gets 25 mature blocks and 25 immature.
|
||||||
|
# Note: To preserve compatibility with older versions of
|
||||||
|
# initialize_chain, only 4 nodes will generate coins.
|
||||||
|
#
|
||||||
|
# blocks are created with timestamps 10 minutes apart
|
||||||
|
# starting from 2010 minutes in the past
|
||||||
|
enable_mocktime()
|
||||||
|
block_time = get_mocktime() - (201 * 10 * 60)
|
||||||
|
for i in range(2):
|
||||||
|
for peer in range(4):
|
||||||
|
for j in range(25):
|
||||||
|
set_node_times(rpcs, block_time)
|
||||||
|
slow_gen(rpcs[peer], 1)
|
||||||
|
block_time += 10*60
|
||||||
|
# Must sync before next peer starts generating blocks
|
||||||
|
sync_blocks(rpcs)
|
||||||
|
|
||||||
|
# Shut them down, and clean up cache directories:
|
||||||
|
stop_nodes(rpcs)
|
||||||
|
wait_navcoinds()
|
||||||
|
disable_mocktime()
|
||||||
|
for i in range(MAX_NODES):
|
||||||
|
os.remove(log_filename("cache", i, "debug.log"))
|
||||||
|
os.remove(log_filename("cache", i, "db.log"))
|
||||||
|
os.remove(log_filename("cache", i, "peers.dat"))
|
||||||
|
os.remove(log_filename("cache", i, "fee_estimates.dat"))
|
||||||
|
|
||||||
|
for i in range(num_nodes):
|
||||||
|
from_dir = os.path.join("cache", "node"+str(i))
|
||||||
|
to_dir = os.path.join(test_dir, "node"+str(i))
|
||||||
|
shutil.copytree(from_dir, to_dir)
|
||||||
|
initialize_datadir(test_dir, i) # Overwrite port/rpcport in navcoin.conf
|
||||||
|
|
||||||
|
def initialize_chain_clean(test_dir, num_nodes):
|
||||||
|
"""
|
||||||
|
Create an empty blockchain and num_nodes wallets.
|
||||||
|
Useful if a test case wants complete control over initialization.
|
||||||
|
"""
|
||||||
|
for i in range(num_nodes):
|
||||||
|
datadir=initialize_datadir(test_dir, i)
|
||||||
|
|
||||||
|
|
||||||
|
def _rpchost_to_args(rpchost):
|
||||||
|
'''Convert optional IP:port spec to rpcconnect/rpcport args'''
|
||||||
|
if rpchost is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
match = re.match('(\[[0-9a-fA-f:]+\]|[^:]+)(?::([0-9]+))?$', rpchost)
|
||||||
|
if not match:
|
||||||
|
raise ValueError('Invalid RPC host spec ' + rpchost)
|
||||||
|
|
||||||
|
rpcconnect = match.group(1)
|
||||||
|
rpcport = match.group(2)
|
||||||
|
|
||||||
|
if rpcconnect.startswith('['): # remove IPv6 [...] wrapping
|
||||||
|
rpcconnect = rpcconnect[1:-1]
|
||||||
|
|
||||||
|
rv = ['-rpcconnect=' + rpcconnect]
|
||||||
|
if rpcport:
|
||||||
|
rv += ['-rpcport=' + rpcport]
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def start_node(i, dirname, extra_args=None, rpchost=None, timewait=None, binary=None):
|
||||||
|
"""
|
||||||
|
Start a navcoind and return RPC connection to it
|
||||||
|
"""
|
||||||
|
datadir = os.path.join(dirname, "node"+str(i))
|
||||||
|
if binary is None:
|
||||||
|
binary = os.getenv("NAVCOIND", "navcoind")
|
||||||
|
args = [ binary, "-datadir="+datadir, "-server", "-keypool=1", "-discover=0", "-rest", "-mocktime="+str(get_mocktime()) ]
|
||||||
|
if extra_args is not None: args.extend(extra_args)
|
||||||
|
navcoind_processes[i] = subprocess.Popen(args)
|
||||||
|
if os.getenv("PYTHON_DEBUG", ""):
|
||||||
|
print("start_node: navcoind started, waiting for RPC to come up")
|
||||||
|
url = rpc_url(i, rpchost)
|
||||||
|
wait_for_navcoind_start(navcoind_processes[i], url, i)
|
||||||
|
if os.getenv("PYTHON_DEBUG", ""):
|
||||||
|
print("start_node: RPC succesfully started")
|
||||||
|
proxy = get_rpc_proxy(url, i, timeout=timewait)
|
||||||
|
|
||||||
|
if COVERAGE_DIR:
|
||||||
|
coverage.write_all_rpc_commands(COVERAGE_DIR, proxy)
|
||||||
|
|
||||||
|
return proxy
|
||||||
|
|
||||||
|
def start_nodes(num_nodes, dirname, extra_args=None, rpchost=None, binary=None):
|
||||||
|
"""
|
||||||
|
Start multiple navcoinds, return RPC connections to them
|
||||||
|
"""
|
||||||
|
if extra_args is None: extra_args = [ None for _ in range(num_nodes) ]
|
||||||
|
if binary is None: binary = [ None for _ in range(num_nodes) ]
|
||||||
|
rpcs = []
|
||||||
|
try:
|
||||||
|
for i in range(num_nodes):
|
||||||
|
rpcs.append(start_node(i, dirname, extra_args[i], rpchost, binary=binary[i]))
|
||||||
|
except: # If one node failed to start, stop the others
|
||||||
|
stop_nodes(rpcs)
|
||||||
|
raise
|
||||||
|
return rpcs
|
||||||
|
|
||||||
|
def log_filename(dirname, n_node, logname):
|
||||||
|
return os.path.join(dirname, "node"+str(n_node), "devnet", logname)
|
||||||
|
|
||||||
|
def stop_node(node, i):
|
||||||
|
try:
|
||||||
|
node.stop()
|
||||||
|
except http.client.CannotSendRequest as e:
|
||||||
|
print("WARN: Unable to stop node: " + repr(e))
|
||||||
|
navcoind_processes[i].wait(timeout=NAVCOIND_PROC_WAIT_TIMEOUT)
|
||||||
|
del navcoind_processes[i]
|
||||||
|
|
||||||
|
def stop_nodes(nodes):
|
||||||
|
for node in nodes:
|
||||||
|
try:
|
||||||
|
node.stop()
|
||||||
|
except http.client.CannotSendRequest as e:
|
||||||
|
print("WARN: Unable to stop node: " + repr(e))
|
||||||
|
del nodes[:] # Emptying array closes connections as a side effect
|
||||||
|
|
||||||
|
def set_node_times(nodes, t):
|
||||||
|
for node in nodes:
|
||||||
|
node.setmocktime(t)
|
||||||
|
|
||||||
|
def wait_navcoinds():
|
||||||
|
# Wait for all navcoinds to cleanly exit
|
||||||
|
for navcoind in navcoind_processes.values():
|
||||||
|
navcoind.wait(timeout=NAVCOIND_PROC_WAIT_TIMEOUT)
|
||||||
|
navcoind_processes.clear()
|
||||||
|
|
||||||
|
def connect_nodes(from_connection, node_num):
|
||||||
|
ip_port = "127.0.0.1:"+str(p2p_port(node_num))
|
||||||
|
from_connection.addnode(ip_port, "onetry")
|
||||||
|
# poll until version handshake complete to avoid race conditions
|
||||||
|
# with transaction relaying
|
||||||
|
while any(peer['version'] == 0 for peer in from_connection.getpeerinfo()):
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
def connect_nodes_bi(nodes, a, b):
|
||||||
|
connect_nodes(nodes[a], b)
|
||||||
|
connect_nodes(nodes[b], a)
|
||||||
|
|
||||||
|
def find_output(node, txid, amount):
|
||||||
|
"""
|
||||||
|
Return index to output of txid with value amount
|
||||||
|
Raises exception if there is none.
|
||||||
|
"""
|
||||||
|
txdata = node.getrawtransaction(txid, 1)
|
||||||
|
for i in range(len(txdata["vout"])):
|
||||||
|
if txdata["vout"][i]["value"] == amount:
|
||||||
|
return i
|
||||||
|
raise RuntimeError("find_output txid %s : %s not found"%(txid,str(amount)))
|
||||||
|
|
||||||
|
|
||||||
|
def gather_inputs(from_node, amount_needed, confirmations_required=1):
|
||||||
|
"""
|
||||||
|
Return a random set of unspent txouts that are enough to pay amount_needed
|
||||||
|
"""
|
||||||
|
assert(confirmations_required >=0)
|
||||||
|
utxo = from_node.listunspent(confirmations_required)
|
||||||
|
random.shuffle(utxo)
|
||||||
|
inputs = []
|
||||||
|
total_in = Decimal("0.00000000")
|
||||||
|
while total_in < amount_needed and len(utxo) > 0:
|
||||||
|
t = utxo.pop()
|
||||||
|
total_in += t["amount"]
|
||||||
|
inputs.append({ "txid" : t["txid"], "vout" : t["vout"], "address" : t["address"] } )
|
||||||
|
if total_in < amount_needed:
|
||||||
|
raise RuntimeError("Insufficient funds: need %d, have %d"%(amount_needed, total_in))
|
||||||
|
return (total_in, inputs)
|
||||||
|
|
||||||
|
def make_change(from_node, amount_in, amount_out, fee):
|
||||||
|
"""
|
||||||
|
Create change output(s), return them
|
||||||
|
"""
|
||||||
|
outputs = {}
|
||||||
|
amount = amount_out+fee
|
||||||
|
change = amount_in - amount
|
||||||
|
if change > amount*2:
|
||||||
|
# Create an extra change output to break up big inputs
|
||||||
|
change_address = from_node.getnewaddress()
|
||||||
|
# Split change in two, being careful of rounding:
|
||||||
|
outputs[change_address] = Decimal(change/2).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN)
|
||||||
|
change = amount_in - amount - outputs[change_address]
|
||||||
|
if change > 0:
|
||||||
|
outputs[from_node.getnewaddress()] = change
|
||||||
|
return outputs
|
||||||
|
|
||||||
|
def send_zeropri_transaction(from_node, to_node, amount, fee):
|
||||||
|
"""
|
||||||
|
Create&broadcast a zero-priority transaction.
|
||||||
|
Returns (txid, hex-encoded-txdata)
|
||||||
|
Ensures transaction is zero-priority by first creating a send-to-self,
|
||||||
|
then using its output
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create a send-to-self with confirmed inputs:
|
||||||
|
self_address = from_node.getnewaddress()
|
||||||
|
(total_in, inputs) = gather_inputs(from_node, amount+fee*2)
|
||||||
|
outputs = make_change(from_node, total_in, amount+fee, fee)
|
||||||
|
outputs[self_address] = float(amount+fee)
|
||||||
|
|
||||||
|
self_rawtx = from_node.createrawtransaction(inputs, outputs)
|
||||||
|
self_signresult = from_node.signrawtransaction(self_rawtx)
|
||||||
|
self_txid = from_node.sendrawtransaction(self_signresult["hex"], True)
|
||||||
|
|
||||||
|
vout = find_output(from_node, self_txid, amount+fee)
|
||||||
|
# Now immediately spend the output to create a 1-input, 1-output
|
||||||
|
# zero-priority transaction:
|
||||||
|
inputs = [ { "txid" : self_txid, "vout" : vout } ]
|
||||||
|
outputs = { to_node.getnewaddress() : float(amount) }
|
||||||
|
|
||||||
|
rawtx = from_node.createrawtransaction(inputs, outputs)
|
||||||
|
signresult = from_node.signrawtransaction(rawtx)
|
||||||
|
txid = from_node.sendrawtransaction(signresult["hex"], True)
|
||||||
|
|
||||||
|
return (txid, signresult["hex"])
|
||||||
|
|
||||||
|
def random_zeropri_transaction(nodes, amount, min_fee, fee_increment, fee_variants):
|
||||||
|
"""
|
||||||
|
Create a random zero-priority transaction.
|
||||||
|
Returns (txid, hex-encoded-transaction-data, fee)
|
||||||
|
"""
|
||||||
|
from_node = random.choice(nodes)
|
||||||
|
to_node = random.choice(nodes)
|
||||||
|
fee = min_fee + fee_increment*random.randint(0,fee_variants)
|
||||||
|
(txid, txhex) = send_zeropri_transaction(from_node, to_node, amount, fee)
|
||||||
|
return (txid, txhex, fee)
|
||||||
|
|
||||||
|
def random_transaction(nodes, amount, min_fee, fee_increment, fee_variants):
|
||||||
|
"""
|
||||||
|
Create a random transaction.
|
||||||
|
Returns (txid, hex-encoded-transaction-data, fee)
|
||||||
|
"""
|
||||||
|
from_node = random.choice(nodes)
|
||||||
|
to_node = random.choice(nodes)
|
||||||
|
fee = min_fee + fee_increment*random.randint(0,fee_variants)
|
||||||
|
|
||||||
|
(total_in, inputs) = gather_inputs(from_node, amount+fee)
|
||||||
|
outputs = make_change(from_node, total_in, amount, fee)
|
||||||
|
outputs[to_node.getnewaddress()] = float(amount)
|
||||||
|
|
||||||
|
rawtx = from_node.createrawtransaction(inputs, outputs)
|
||||||
|
signresult = from_node.signrawtransaction(rawtx)
|
||||||
|
txid = from_node.sendrawtransaction(signresult["hex"], True)
|
||||||
|
|
||||||
|
return (txid, signresult["hex"], fee)
|
||||||
|
|
||||||
|
def assert_fee_amount(fee, tx_size, fee_per_kB):
|
||||||
|
"""Assert the fee was in range"""
|
||||||
|
target_fee = tx_size * fee_per_kB / 1000
|
||||||
|
if fee < target_fee:
|
||||||
|
raise AssertionError("Fee of %s NAV too low! (Should be %s NAV)"%(str(fee), str(target_fee)))
|
||||||
|
# allow the wallet's estimation to be at most 2 bytes off
|
||||||
|
if fee > (tx_size + 2) * fee_per_kB / 1000:
|
||||||
|
raise AssertionError("Fee of %s NAV too high! (Should be %s NAV)"%(str(fee), str(target_fee)))
|
||||||
|
|
||||||
|
def assert_equal(thing1, thing2):
|
||||||
|
if thing1 != thing2:
|
||||||
|
raise AssertionError("%s != %s"%(str(thing1),str(thing2)))
|
||||||
|
|
||||||
|
def assert_greater_than(thing1, thing2):
|
||||||
|
if thing1 <= thing2:
|
||||||
|
raise AssertionError("%s <= %s"%(str(thing1),str(thing2)))
|
||||||
|
|
||||||
|
def assert_raises(exc, fun, *args, **kwds):
|
||||||
|
try:
|
||||||
|
fun(*args, **kwds)
|
||||||
|
except exc:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
raise AssertionError("Unexpected exception raised: "+type(e).__name__)
|
||||||
|
else:
|
||||||
|
raise AssertionError("No exception raised")
|
||||||
|
|
||||||
|
def assert_is_hex_string(string):
|
||||||
|
try:
|
||||||
|
int(string, 16)
|
||||||
|
except Exception as e:
|
||||||
|
raise AssertionError(
|
||||||
|
"Couldn't interpret %r as hexadecimal; raised: %s" % (string, e))
|
||||||
|
|
||||||
|
def assert_is_hash_string(string, length=64):
|
||||||
|
if not isinstance(string, str):
|
||||||
|
raise AssertionError("Expected a string, got type %r" % type(string))
|
||||||
|
elif length and len(string) != length:
|
||||||
|
raise AssertionError(
|
||||||
|
"String of length %d expected; got %d" % (length, len(string)))
|
||||||
|
elif not re.match('[abcdef0-9]+$', string):
|
||||||
|
raise AssertionError(
|
||||||
|
"String %r contains invalid characters for a hash." % string)
|
||||||
|
|
||||||
|
def assert_array_result(object_array, to_match, expected, should_not_find = False):
|
||||||
|
"""
|
||||||
|
Pass in array of JSON objects, a dictionary with key/value pairs
|
||||||
|
to match against, and another dictionary with expected key/value
|
||||||
|
pairs.
|
||||||
|
If the should_not_find flag is true, to_match should not be found
|
||||||
|
in object_array
|
||||||
|
"""
|
||||||
|
if should_not_find == True:
|
||||||
|
assert_equal(expected, { })
|
||||||
|
num_matched = 0
|
||||||
|
for item in object_array:
|
||||||
|
all_match = True
|
||||||
|
for key,value in to_match.items():
|
||||||
|
if item[key] != value:
|
||||||
|
all_match = False
|
||||||
|
if not all_match:
|
||||||
|
continue
|
||||||
|
elif should_not_find == True:
|
||||||
|
num_matched = num_matched+1
|
||||||
|
for key,value in expected.items():
|
||||||
|
if item[key] != value:
|
||||||
|
raise AssertionError("%s : expected %s=%s"%(str(item), str(key), str(value)))
|
||||||
|
num_matched = num_matched+1
|
||||||
|
if num_matched == 0 and should_not_find != True:
|
||||||
|
raise AssertionError("No objects matched %s"%(str(to_match)))
|
||||||
|
if num_matched > 0 and should_not_find == True:
|
||||||
|
raise AssertionError("Objects were found %s"%(str(to_match)))
|
||||||
|
|
||||||
|
def assert_raises_rpc_error(code, message, fun, *args, **kwds):
|
||||||
|
"""Run an RPC and verify that a specific JSONRPC exception code and message is raised.
|
||||||
|
Calls function `fun` with arguments `args` and `kwds`. Catches a JSONRPCException
|
||||||
|
and verifies that the error code and message are as expected. Throws AssertionError if
|
||||||
|
no JSONRPCException was raised or if the error code/message are not as expected.
|
||||||
|
Args:
|
||||||
|
code (int), optional: the error code returned by the RPC call (defined
|
||||||
|
in src/rpc/protocol.h). Set to None if checking the error code is not required.
|
||||||
|
message (string), optional: [a substring of] the error string returned by the
|
||||||
|
RPC call. Set to None if checking the error string is not required.
|
||||||
|
fun (function): the function to call. This should be the name of an RPC.
|
||||||
|
args*: positional arguments for the function.
|
||||||
|
kwds**: named arguments for the function.
|
||||||
|
"""
|
||||||
|
assert try_rpc(code, message, fun, *args, **kwds), "No exception raised"
|
||||||
|
|
||||||
|
def try_rpc(code, message, fun, *args, **kwds):
|
||||||
|
"""Tries to run an rpc command.
|
||||||
|
Test against error code and message if the rpc fails.
|
||||||
|
Returns whether a JSONRPCException was raised."""
|
||||||
|
try:
|
||||||
|
fun(*args, **kwds)
|
||||||
|
except JSONRPCException as e:
|
||||||
|
# JSONRPCException was thrown as expected. Check the code and message values are correct.
|
||||||
|
if (code is not None) and (code != e.error["code"]):
|
||||||
|
raise AssertionError("Unexpected JSONRPC error code %i" % e.error["code"])
|
||||||
|
if (message is not None) and (message not in e.error['message']):
|
||||||
|
raise AssertionError("Expected substring not found:" + e.error['message'])
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
raise AssertionError("Unexpected exception raised: " + type(e).__name__)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def satoshi_round(amount):
|
||||||
|
return Decimal(amount).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN)
|
||||||
|
|
||||||
|
# Helper to create at least "count" utxos
|
||||||
|
# Pass in a fee that is sufficient for relay and mining new transactions.
|
||||||
|
def create_confirmed_utxos(fee, node, count):
|
||||||
|
node.generate(int(0.5*count)+101)
|
||||||
|
utxos = node.listunspent()
|
||||||
|
iterations = count - len(utxos)
|
||||||
|
addr1 = node.getnewaddress()
|
||||||
|
addr2 = node.getnewaddress()
|
||||||
|
if iterations <= 0:
|
||||||
|
return utxos
|
||||||
|
for i in range(iterations):
|
||||||
|
t = utxos.pop()
|
||||||
|
inputs = []
|
||||||
|
inputs.append({ "txid" : t["txid"], "vout" : t["vout"]})
|
||||||
|
outputs = {}
|
||||||
|
send_value = t['amount'] - fee
|
||||||
|
outputs[addr1] = satoshi_round(send_value/2)
|
||||||
|
outputs[addr2] = satoshi_round(send_value/2)
|
||||||
|
raw_tx = node.createrawtransaction(inputs, outputs)
|
||||||
|
signed_tx = node.signrawtransaction(raw_tx)["hex"]
|
||||||
|
txid = node.sendrawtransaction(signed_tx)
|
||||||
|
|
||||||
|
while (node.getmempoolinfo()['size'] > 0):
|
||||||
|
node.generate(1)
|
||||||
|
|
||||||
|
utxos = node.listunspent()
|
||||||
|
assert(len(utxos) >= count)
|
||||||
|
return utxos
|
||||||
|
|
||||||
|
# Create large OP_RETURN txouts that can be appended to a transaction
|
||||||
|
# to make it large (helper for constructing large transactions).
|
||||||
|
def gen_return_txouts():
|
||||||
|
# Some pre-processing to create a bunch of OP_RETURN txouts to insert into transactions we create
|
||||||
|
# So we have big transactions (and therefore can't fit very many into each block)
|
||||||
|
# create one script_pubkey
|
||||||
|
script_pubkey = "6a4d0200" #OP_RETURN OP_PUSH2 512 bytes
|
||||||
|
for i in range (512):
|
||||||
|
script_pubkey = script_pubkey + "01"
|
||||||
|
# concatenate 128 txouts of above script_pubkey which we'll insert before the txout for change
|
||||||
|
txouts = "81"
|
||||||
|
for k in range(128):
|
||||||
|
# add txout value
|
||||||
|
txouts = txouts + "0000000000000000"
|
||||||
|
# add length of script_pubkey
|
||||||
|
txouts = txouts + "fd0402"
|
||||||
|
# add script_pubkey
|
||||||
|
txouts = txouts + script_pubkey
|
||||||
|
return txouts
|
||||||
|
|
||||||
|
def create_tx(node, coinbase, to_address, amount):
|
||||||
|
inputs = [{ "txid" : coinbase, "vout" : 0}]
|
||||||
|
outputs = { to_address : amount }
|
||||||
|
rawtx = node.createrawtransaction(inputs, outputs)
|
||||||
|
signresult = node.signrawtransaction(rawtx)
|
||||||
|
assert_equal(signresult["complete"], True)
|
||||||
|
return signresult["hex"]
|
||||||
|
|
||||||
|
# Create a spend of each passed-in utxo, splicing in "txouts" to each raw
|
||||||
|
# transaction to make it large. See gen_return_txouts() above.
|
||||||
|
def create_lots_of_big_transactions(node, txouts, utxos, fee):
|
||||||
|
addr = node.getnewaddress()
|
||||||
|
txids = []
|
||||||
|
for i in range(len(utxos)):
|
||||||
|
t = utxos.pop()
|
||||||
|
inputs = []
|
||||||
|
inputs.append({ "txid" : t["txid"], "vout" : t["vout"]})
|
||||||
|
outputs = {}
|
||||||
|
send_value = t['amount'] - fee
|
||||||
|
outputs[addr] = satoshi_round(send_value)
|
||||||
|
rawtx = node.createrawtransaction(inputs, outputs)
|
||||||
|
newtx = rawtx[0:92]
|
||||||
|
newtx = newtx + txouts
|
||||||
|
newtx = newtx + rawtx[94:]
|
||||||
|
signresult = node.signrawtransaction(newtx, None, None, "NONE")
|
||||||
|
txid = node.sendrawtransaction(signresult["hex"], True)
|
||||||
|
txids.append(txid)
|
||||||
|
return txids
|
||||||
|
|
||||||
|
def get_bip9_status(node, key):
|
||||||
|
info = node.getblockchaininfo()
|
||||||
|
return info['bip9_softforks'][key]
|
||||||
|
|
||||||
|
|
||||||
|
def slow_gen(node, count, sleep = 0.1):
|
||||||
|
total = count
|
||||||
|
blocks = []
|
||||||
|
while total > 0:
|
||||||
|
now = min(total, 10)
|
||||||
|
blocks.extend(node.generate(now))
|
||||||
|
total -= now
|
||||||
|
time.sleep(sleep)
|
||||||
|
return blocks
|
||||||
180
basicswap/interface/contrib/pivx_test_framework/authproxy.py
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
# Copyright (c) 2011 Jeff Garzik
|
||||||
|
#
|
||||||
|
# Previous copyright, from python-jsonrpc/jsonrpc/proxy.py:
|
||||||
|
#
|
||||||
|
# Copyright (c) 2007 Jan-Klaas Kollhof
|
||||||
|
#
|
||||||
|
# This file is part of jsonrpc.
|
||||||
|
#
|
||||||
|
# jsonrpc is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2.1 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This software is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with this software; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
"""HTTP proxy for opening RPC connection to pivxd.
|
||||||
|
|
||||||
|
AuthServiceProxy has the following improvements over python-jsonrpc's
|
||||||
|
ServiceProxy class:
|
||||||
|
|
||||||
|
- HTTP connections persist for the life of the AuthServiceProxy object
|
||||||
|
(if server supports HTTP/1.1)
|
||||||
|
- sends protocol 'version', per JSON-RPC 1.1
|
||||||
|
- sends proper, incrementing 'id'
|
||||||
|
- sends Basic HTTP authentication headers
|
||||||
|
- parses all JSON numbers that look like floats as Decimal
|
||||||
|
- uses standard Python json lib
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import decimal
|
||||||
|
import http.client
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
HTTP_TIMEOUT = 300
|
||||||
|
USER_AGENT = "AuthServiceProxy/0.1"
|
||||||
|
|
||||||
|
log = logging.getLogger("BitcoinRPC")
|
||||||
|
|
||||||
|
class JSONRPCException(Exception):
|
||||||
|
def __init__(self, rpc_error):
|
||||||
|
try:
|
||||||
|
errmsg = '%(message)s (%(code)i)' % rpc_error
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
errmsg = ''
|
||||||
|
super().__init__(errmsg)
|
||||||
|
self.error = rpc_error
|
||||||
|
|
||||||
|
|
||||||
|
def EncodeDecimal(o):
|
||||||
|
if isinstance(o, decimal.Decimal):
|
||||||
|
return str(o)
|
||||||
|
raise TypeError(repr(o) + " is not JSON serializable")
|
||||||
|
|
||||||
|
class AuthServiceProxy():
|
||||||
|
__id_count = 0
|
||||||
|
|
||||||
|
# ensure_ascii: escape unicode as \uXXXX, passed to json.dumps
|
||||||
|
def __init__(self, service_url, service_name=None, timeout=HTTP_TIMEOUT, connection=None, ensure_ascii=True):
|
||||||
|
self.__service_url = service_url
|
||||||
|
self._service_name = service_name
|
||||||
|
self.ensure_ascii = ensure_ascii # can be toggled on the fly by tests
|
||||||
|
self.__url = urllib.parse.urlparse(service_url)
|
||||||
|
port = 80 if self.__url.port is None else self.__url.port
|
||||||
|
user = None if self.__url.username is None else self.__url.username.encode('utf8')
|
||||||
|
passwd = None if self.__url.password is None else self.__url.password.encode('utf8')
|
||||||
|
authpair = user + b':' + passwd
|
||||||
|
self.__auth_header = b'Basic ' + base64.b64encode(authpair)
|
||||||
|
|
||||||
|
if connection:
|
||||||
|
# Callables re-use the connection of the original proxy
|
||||||
|
self.__conn = connection
|
||||||
|
elif self.__url.scheme == 'https':
|
||||||
|
self.__conn = http.client.HTTPSConnection(self.__url.hostname, port, timeout=timeout)
|
||||||
|
else:
|
||||||
|
self.__conn = http.client.HTTPConnection(self.__url.hostname, port, timeout=timeout)
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
if name.startswith('__') and name.endswith('__'):
|
||||||
|
# Python internal stuff
|
||||||
|
raise AttributeError
|
||||||
|
if self._service_name is not None:
|
||||||
|
name = "%s.%s" % (self._service_name, name)
|
||||||
|
return AuthServiceProxy(self.__service_url, name, connection=self.__conn)
|
||||||
|
|
||||||
|
def _request(self, method, path, postdata):
|
||||||
|
'''
|
||||||
|
Do a HTTP request, with retry if we get disconnected (e.g. due to a timeout).
|
||||||
|
This is a workaround for https://bugs.python.org/issue3566 which is fixed in Python 3.5.
|
||||||
|
'''
|
||||||
|
headers = {'Host': self.__url.hostname,
|
||||||
|
'User-Agent': USER_AGENT,
|
||||||
|
'Authorization': self.__auth_header,
|
||||||
|
'Content-type': 'application/json'}
|
||||||
|
try:
|
||||||
|
self.__conn.request(method, path, postdata, headers)
|
||||||
|
return self._get_response()
|
||||||
|
except http.client.BadStatusLine as e:
|
||||||
|
if e.line == "''": # if connection was closed, try again
|
||||||
|
self.__conn.close()
|
||||||
|
self.__conn.request(method, path, postdata, headers)
|
||||||
|
return self._get_response()
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
except (BrokenPipeError, ConnectionResetError):
|
||||||
|
# Python 3.5+ raises BrokenPipeError instead of BadStatusLine when the connection was reset
|
||||||
|
# ConnectionResetError happens on FreeBSD with Python 3.4
|
||||||
|
self.__conn.close()
|
||||||
|
self.__conn.request(method, path, postdata, headers)
|
||||||
|
return self._get_response()
|
||||||
|
|
||||||
|
def get_request(self, *args, **argsn):
|
||||||
|
AuthServiceProxy.__id_count += 1
|
||||||
|
|
||||||
|
log.debug("-%s-> %s %s" % (AuthServiceProxy.__id_count, self._service_name,
|
||||||
|
json.dumps(args, default=EncodeDecimal, ensure_ascii=self.ensure_ascii)))
|
||||||
|
if args and argsn:
|
||||||
|
raise ValueError('Cannot handle both named and positional arguments')
|
||||||
|
return {'version': '1.1',
|
||||||
|
'method': self._service_name,
|
||||||
|
'params': args or argsn,
|
||||||
|
'id': AuthServiceProxy.__id_count}
|
||||||
|
|
||||||
|
def __call__(self, *args, **argsn):
|
||||||
|
postdata = json.dumps(self.get_request(*args, **argsn), default=EncodeDecimal, ensure_ascii=self.ensure_ascii)
|
||||||
|
response = self._request('POST', self.__url.path, postdata.encode('utf-8'))
|
||||||
|
if response['error'] is not None:
|
||||||
|
raise JSONRPCException(response['error'])
|
||||||
|
elif 'result' not in response:
|
||||||
|
raise JSONRPCException({
|
||||||
|
'code': -343, 'message': 'missing JSON-RPC result'})
|
||||||
|
else:
|
||||||
|
return response['result']
|
||||||
|
|
||||||
|
def batch(self, rpc_call_list):
|
||||||
|
postdata = json.dumps(list(rpc_call_list), default=EncodeDecimal, ensure_ascii=self.ensure_ascii)
|
||||||
|
log.debug("--> " + postdata)
|
||||||
|
return self._request('POST', self.__url.path, postdata.encode('utf-8'))
|
||||||
|
|
||||||
|
def _get_response(self):
|
||||||
|
req_start_time = time.time()
|
||||||
|
try:
|
||||||
|
http_response = self.__conn.getresponse()
|
||||||
|
except socket.timeout:
|
||||||
|
raise JSONRPCException({
|
||||||
|
'code': -344,
|
||||||
|
'message': '%r RPC took longer than %f seconds. Consider '
|
||||||
|
'using larger timeout for calls that take '
|
||||||
|
'longer to return.' % (self._service_name,
|
||||||
|
self.__conn.timeout)})
|
||||||
|
if http_response is None:
|
||||||
|
raise JSONRPCException({
|
||||||
|
'code': -342, 'message': 'missing HTTP response from server'})
|
||||||
|
|
||||||
|
content_type = http_response.getheader('Content-Type')
|
||||||
|
if content_type != 'application/json':
|
||||||
|
raise JSONRPCException({
|
||||||
|
'code': -342, 'message': 'non-JSON HTTP response with \'%i %s\' from server' % (http_response.status, http_response.reason)})
|
||||||
|
|
||||||
|
responsedata = http_response.read().decode('utf8')
|
||||||
|
response = json.loads(responsedata, parse_float=decimal.Decimal)
|
||||||
|
elapsed = time.time() - req_start_time
|
||||||
|
if "error" in response and response["error"] is None:
|
||||||
|
log.debug("<-%s- [%.6f] %s" % (response["id"], elapsed, json.dumps(response["result"], default=EncodeDecimal, ensure_ascii=self.ensure_ascii)))
|
||||||
|
else:
|
||||||
|
log.debug("<-- [%.6f] %s" % (elapsed, responsedata))
|
||||||
|
return response
|
||||||
|
|
||||||
|
def __truediv__(self, relative_uri):
|
||||||
|
return AuthServiceProxy("{}/{}".format(self.__service_url, relative_uri), self._service_name, connection=self.__conn)
|
||||||
109
basicswap/interface/contrib/pivx_test_framework/coverage.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright (c) 2015-2017 The Bitcoin Core developers
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
"""Utilities for doing coverage analysis on the RPC interface.
|
||||||
|
|
||||||
|
Provides a way to track which RPC commands are exercised during
|
||||||
|
testing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
REFERENCE_FILENAME = 'rpc_interface.txt'
|
||||||
|
|
||||||
|
|
||||||
|
class AuthServiceProxyWrapper():
|
||||||
|
"""
|
||||||
|
An object that wraps AuthServiceProxy to record specific RPC calls.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, auth_service_proxy_instance, coverage_logfile=None):
|
||||||
|
"""
|
||||||
|
Kwargs:
|
||||||
|
auth_service_proxy_instance (AuthServiceProxy): the instance
|
||||||
|
being wrapped.
|
||||||
|
coverage_logfile (str): if specified, write each service_name
|
||||||
|
out to a file when called.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.auth_service_proxy_instance = auth_service_proxy_instance
|
||||||
|
self.coverage_logfile = coverage_logfile
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return_val = getattr(self.auth_service_proxy_instance, name)
|
||||||
|
if not isinstance(return_val, type(self.auth_service_proxy_instance)):
|
||||||
|
# If proxy getattr returned an unwrapped value, do the same here.
|
||||||
|
return return_val
|
||||||
|
return AuthServiceProxyWrapper(return_val, self.coverage_logfile)
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Delegates to AuthServiceProxy, then writes the particular RPC method
|
||||||
|
called to a file.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return_val = self.auth_service_proxy_instance.__call__(*args, **kwargs)
|
||||||
|
self._log_call()
|
||||||
|
return return_val
|
||||||
|
|
||||||
|
def _log_call(self):
|
||||||
|
rpc_method = self.auth_service_proxy_instance._service_name
|
||||||
|
|
||||||
|
if self.coverage_logfile:
|
||||||
|
with open(self.coverage_logfile, 'a+', encoding='utf8') as f:
|
||||||
|
f.write("%s\n" % rpc_method)
|
||||||
|
|
||||||
|
def __truediv__(self, relative_uri):
|
||||||
|
return AuthServiceProxyWrapper(self.auth_service_proxy_instance / relative_uri,
|
||||||
|
self.coverage_logfile)
|
||||||
|
|
||||||
|
def get_request(self, *args, **kwargs):
|
||||||
|
self._log_call()
|
||||||
|
return self.auth_service_proxy_instance.get_request(*args)
|
||||||
|
|
||||||
|
def get_filename(dirname, n_node):
|
||||||
|
"""
|
||||||
|
Get a filename unique to the test process ID and node.
|
||||||
|
|
||||||
|
This file will contain a list of RPC commands covered.
|
||||||
|
"""
|
||||||
|
pid = str(os.getpid())
|
||||||
|
return os.path.join(
|
||||||
|
dirname, "coverage.pid%s.node%s.txt" % (pid, str(n_node)))
|
||||||
|
|
||||||
|
|
||||||
|
def write_all_rpc_commands(dirname, node):
|
||||||
|
"""
|
||||||
|
Write out a list of all RPC functions available in `pivx-cli` for
|
||||||
|
coverage comparison. This will only happen once per coverage
|
||||||
|
directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dirname (str): temporary test dir
|
||||||
|
node (AuthServiceProxy): client
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool. if the RPC interface file was written.
|
||||||
|
|
||||||
|
"""
|
||||||
|
filename = os.path.join(dirname, REFERENCE_FILENAME)
|
||||||
|
|
||||||
|
if os.path.isfile(filename):
|
||||||
|
return False
|
||||||
|
|
||||||
|
help_output = node.help().split('\n')
|
||||||
|
commands = set()
|
||||||
|
|
||||||
|
for line in help_output:
|
||||||
|
line = line.strip()
|
||||||
|
|
||||||
|
# Ignore blanks and headers
|
||||||
|
if line and not line.startswith('='):
|
||||||
|
commands.add("%s\n" % line.split()[0])
|
||||||
|
|
||||||
|
with open(filename, 'w', encoding='utf8') as f:
|
||||||
|
f.writelines(list(commands))
|
||||||
|
|
||||||
|
return True
|
||||||
1580
basicswap/interface/contrib/pivx_test_framework/messages.py
Executable file
63
basicswap/interface/contrib/pivx_test_framework/siphash.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright (c) 2016-2017 The Bitcoin Core developers
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
"""Specialized SipHash-2-4 implementations.
|
||||||
|
|
||||||
|
This implements SipHash-2-4 for 256-bit integers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def rotl64(n, b):
|
||||||
|
return n >> (64 - b) | (n & ((1 << (64 - b)) - 1)) << b
|
||||||
|
|
||||||
|
def siphash_round(v0, v1, v2, v3):
|
||||||
|
v0 = (v0 + v1) & ((1 << 64) - 1)
|
||||||
|
v1 = rotl64(v1, 13)
|
||||||
|
v1 ^= v0
|
||||||
|
v0 = rotl64(v0, 32)
|
||||||
|
v2 = (v2 + v3) & ((1 << 64) - 1)
|
||||||
|
v3 = rotl64(v3, 16)
|
||||||
|
v3 ^= v2
|
||||||
|
v0 = (v0 + v3) & ((1 << 64) - 1)
|
||||||
|
v3 = rotl64(v3, 21)
|
||||||
|
v3 ^= v0
|
||||||
|
v2 = (v2 + v1) & ((1 << 64) - 1)
|
||||||
|
v1 = rotl64(v1, 17)
|
||||||
|
v1 ^= v2
|
||||||
|
v2 = rotl64(v2, 32)
|
||||||
|
return (v0, v1, v2, v3)
|
||||||
|
|
||||||
|
def siphash256(k0, k1, h):
|
||||||
|
n0 = h & ((1 << 64) - 1)
|
||||||
|
n1 = (h >> 64) & ((1 << 64) - 1)
|
||||||
|
n2 = (h >> 128) & ((1 << 64) - 1)
|
||||||
|
n3 = (h >> 192) & ((1 << 64) - 1)
|
||||||
|
v0 = 0x736f6d6570736575 ^ k0
|
||||||
|
v1 = 0x646f72616e646f6d ^ k1
|
||||||
|
v2 = 0x6c7967656e657261 ^ k0
|
||||||
|
v3 = 0x7465646279746573 ^ k1 ^ n0
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0 ^= n0
|
||||||
|
v3 ^= n1
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0 ^= n1
|
||||||
|
v3 ^= n2
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0 ^= n2
|
||||||
|
v3 ^= n3
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0 ^= n3
|
||||||
|
v3 ^= 0x2000000000000000
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0 ^= 0x2000000000000000
|
||||||
|
v2 ^= 0xFF
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
|
||||||
|
return v0 ^ v1 ^ v2 ^ v3
|
||||||
625
basicswap/interface/contrib/pivx_test_framework/util.py
Normal file
@@ -0,0 +1,625 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright (c) 2014-2017 The Bitcoin Core developers
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
"""Helpful routines for regression testing."""
|
||||||
|
|
||||||
|
from base64 import b64encode
|
||||||
|
from binascii import hexlify, unhexlify
|
||||||
|
from decimal import Decimal, ROUND_DOWN
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
from subprocess import CalledProcessError
|
||||||
|
import time
|
||||||
|
|
||||||
|
from . import coverage, messages
|
||||||
|
from .authproxy import AuthServiceProxy, JSONRPCException
|
||||||
|
|
||||||
|
logger = logging.getLogger("TestFramework.utils")
|
||||||
|
|
||||||
|
# Assert functions
|
||||||
|
##################
|
||||||
|
|
||||||
|
def assert_fee_amount(fee, tx_size, fee_per_kB):
|
||||||
|
"""Assert the fee was in range"""
|
||||||
|
target_fee = round(tx_size * fee_per_kB / 1000, 8)
|
||||||
|
if fee < target_fee:
|
||||||
|
raise AssertionError("Fee of %s PIV too low! (Should be %s PIV)" % (str(fee), str(target_fee)))
|
||||||
|
# allow the wallet's estimation to be at most 2 bytes off
|
||||||
|
if fee > (tx_size + 20) * fee_per_kB / 1000:
|
||||||
|
raise AssertionError("Fee of %s PIV too high! (Should be %s PIV)" % (str(fee), str(target_fee)))
|
||||||
|
|
||||||
|
def assert_equal(thing1, thing2, *args):
|
||||||
|
if thing1 != thing2 or any(thing1 != arg for arg in args):
|
||||||
|
raise AssertionError("not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + args))
|
||||||
|
|
||||||
|
def assert_true(condition, message = ""):
|
||||||
|
if not condition:
|
||||||
|
raise AssertionError(message)
|
||||||
|
|
||||||
|
def assert_false(condition, message = ""):
|
||||||
|
assert_true(not condition, message)
|
||||||
|
|
||||||
|
def assert_greater_than(thing1, thing2):
|
||||||
|
if thing1 <= thing2:
|
||||||
|
raise AssertionError("%s <= %s" % (str(thing1), str(thing2)))
|
||||||
|
|
||||||
|
def assert_greater_than_or_equal(thing1, thing2):
|
||||||
|
if thing1 < thing2:
|
||||||
|
raise AssertionError("%s < %s" % (str(thing1), str(thing2)))
|
||||||
|
|
||||||
|
def assert_raises(exc, fun, *args, **kwds):
|
||||||
|
assert_raises_message(exc, None, fun, *args, **kwds)
|
||||||
|
|
||||||
|
def assert_raises_message(exc, message, fun, *args, **kwds):
|
||||||
|
try:
|
||||||
|
fun(*args, **kwds)
|
||||||
|
except JSONRPCException:
|
||||||
|
raise AssertionError("Use assert_raises_rpc_error() to test RPC failures")
|
||||||
|
except exc as e:
|
||||||
|
if message is not None and message not in e.error['message']:
|
||||||
|
raise AssertionError("Expected substring not found:" + e.error['message'])
|
||||||
|
except Exception as e:
|
||||||
|
raise AssertionError("Unexpected exception raised: " + type(e).__name__)
|
||||||
|
else:
|
||||||
|
raise AssertionError("No exception raised")
|
||||||
|
|
||||||
|
def assert_raises_process_error(returncode, output, fun, *args, **kwds):
|
||||||
|
"""Execute a process and asserts the process return code and output.
|
||||||
|
|
||||||
|
Calls function `fun` with arguments `args` and `kwds`. Catches a CalledProcessError
|
||||||
|
and verifies that the return code and output are as expected. Throws AssertionError if
|
||||||
|
no CalledProcessError was raised or if the return code and output are not as expected.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
returncode (int): the process return code.
|
||||||
|
output (string): [a substring of] the process output.
|
||||||
|
fun (function): the function to call. This should execute a process.
|
||||||
|
args*: positional arguments for the function.
|
||||||
|
kwds**: named arguments for the function.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
fun(*args, **kwds)
|
||||||
|
except CalledProcessError as e:
|
||||||
|
if returncode != e.returncode:
|
||||||
|
raise AssertionError("Unexpected returncode %i" % e.returncode)
|
||||||
|
if output not in e.output:
|
||||||
|
raise AssertionError("Expected substring not found:" + e.output)
|
||||||
|
else:
|
||||||
|
raise AssertionError("No exception raised")
|
||||||
|
|
||||||
|
def assert_raises_rpc_error(code, message, fun, *args, **kwds):
|
||||||
|
"""Run an RPC and verify that a specific JSONRPC exception code and message is raised.
|
||||||
|
|
||||||
|
Calls function `fun` with arguments `args` and `kwds`. Catches a JSONRPCException
|
||||||
|
and verifies that the error code and message are as expected. Throws AssertionError if
|
||||||
|
no JSONRPCException was raised or if the error code/message are not as expected.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
code (int), optional: the error code returned by the RPC call (defined
|
||||||
|
in src/rpc/protocol.h). Set to None if checking the error code is not required.
|
||||||
|
message (string), optional: [a substring of] the error string returned by the
|
||||||
|
RPC call. Set to None if checking the error string is not required.
|
||||||
|
fun (function): the function to call. This should be the name of an RPC.
|
||||||
|
args*: positional arguments for the function.
|
||||||
|
kwds**: named arguments for the function.
|
||||||
|
"""
|
||||||
|
assert try_rpc(code, message, fun, *args, **kwds), "No exception raised"
|
||||||
|
|
||||||
|
def try_rpc(code, message, fun, *args, **kwds):
|
||||||
|
"""Tries to run an rpc command.
|
||||||
|
|
||||||
|
Test against error code and message if the rpc fails.
|
||||||
|
Returns whether a JSONRPCException was raised."""
|
||||||
|
try:
|
||||||
|
fun(*args, **kwds)
|
||||||
|
except JSONRPCException as e:
|
||||||
|
# JSONRPCException was thrown as expected. Check the code and message values are correct.
|
||||||
|
if (code is not None) and (code != e.error["code"]):
|
||||||
|
raise AssertionError("Unexpected JSONRPC error code %i" % e.error["code"])
|
||||||
|
if (message is not None) and (message not in e.error['message']):
|
||||||
|
raise AssertionError("Expected substring (%s) not found in: %s" % (message, e.error['message']))
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
raise AssertionError("Unexpected exception raised: " + type(e).__name__)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def assert_is_hex_string(string):
|
||||||
|
try:
|
||||||
|
int(string, 16)
|
||||||
|
except Exception as e:
|
||||||
|
raise AssertionError(
|
||||||
|
"Couldn't interpret %r as hexadecimal; raised: %s" % (string, e))
|
||||||
|
|
||||||
|
def assert_is_hash_string(string, length=64):
|
||||||
|
if not isinstance(string, str):
|
||||||
|
raise AssertionError("Expected a string, got type %r" % type(string))
|
||||||
|
elif length and len(string) != length:
|
||||||
|
raise AssertionError(
|
||||||
|
"String of length %d expected; got %d" % (length, len(string)))
|
||||||
|
elif not re.match('[abcdef0-9]+$', string):
|
||||||
|
raise AssertionError(
|
||||||
|
"String %r contains invalid characters for a hash." % string)
|
||||||
|
|
||||||
|
def assert_array_result(object_array, to_match, expected, should_not_find=False):
|
||||||
|
"""
|
||||||
|
Pass in array of JSON objects, a dictionary with key/value pairs
|
||||||
|
to match against, and another dictionary with expected key/value
|
||||||
|
pairs.
|
||||||
|
If the should_not_find flag is true, to_match should not be found
|
||||||
|
in object_array
|
||||||
|
"""
|
||||||
|
if should_not_find:
|
||||||
|
assert_equal(expected, {})
|
||||||
|
num_matched = 0
|
||||||
|
for item in object_array:
|
||||||
|
all_match = True
|
||||||
|
for key, value in to_match.items():
|
||||||
|
if item[key] != value:
|
||||||
|
all_match = False
|
||||||
|
if not all_match:
|
||||||
|
continue
|
||||||
|
elif should_not_find:
|
||||||
|
num_matched = num_matched + 1
|
||||||
|
for key, value in expected.items():
|
||||||
|
if item[key] != value:
|
||||||
|
raise AssertionError("%s : expected %s=%s" % (str(item), str(key), str(value)))
|
||||||
|
num_matched = num_matched + 1
|
||||||
|
if num_matched == 0 and not should_not_find:
|
||||||
|
raise AssertionError("No objects matched %s" % (str(to_match)))
|
||||||
|
if num_matched > 0 and should_not_find:
|
||||||
|
raise AssertionError("Objects were found %s" % (str(to_match)))
|
||||||
|
|
||||||
|
# Utility functions
|
||||||
|
###################
|
||||||
|
|
||||||
|
def check_json_precision():
|
||||||
|
"""Make sure json library being used does not lose precision converting BTC values"""
|
||||||
|
n = Decimal("20000000.00000003")
|
||||||
|
satoshis = int(json.loads(json.dumps(float(n))) * 1.0e8)
|
||||||
|
if satoshis != 2000000000000003:
|
||||||
|
raise RuntimeError("JSON encode/decode loses precision")
|
||||||
|
|
||||||
|
def count_bytes(hex_string):
|
||||||
|
return len(bytearray.fromhex(hex_string))
|
||||||
|
|
||||||
|
def bytes_to_hex_str(byte_str):
|
||||||
|
return hexlify(byte_str).decode('ascii')
|
||||||
|
|
||||||
|
def hash256(byte_str):
|
||||||
|
sha256 = hashlib.sha256()
|
||||||
|
sha256.update(byte_str)
|
||||||
|
sha256d = hashlib.sha256()
|
||||||
|
sha256d.update(sha256.digest())
|
||||||
|
return sha256d.digest()[::-1]
|
||||||
|
|
||||||
|
def hex_str_to_bytes(hex_str):
|
||||||
|
return unhexlify(hex_str.encode('ascii'))
|
||||||
|
|
||||||
|
def str_to_b64str(string):
|
||||||
|
return b64encode(string.encode('utf-8')).decode('ascii')
|
||||||
|
|
||||||
|
def satoshi_round(amount):
|
||||||
|
return Decimal(amount).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN)
|
||||||
|
|
||||||
|
def wait_until(predicate,
|
||||||
|
*,
|
||||||
|
attempts=float('inf'),
|
||||||
|
timeout=float('inf'),
|
||||||
|
lock=None,
|
||||||
|
sendpings=None,
|
||||||
|
mocktime=None):
|
||||||
|
|
||||||
|
if attempts == float('inf') and timeout == float('inf'):
|
||||||
|
timeout = 60
|
||||||
|
attempt = 0
|
||||||
|
timeout += time.time()
|
||||||
|
|
||||||
|
while attempt < attempts and time.time() < timeout:
|
||||||
|
if lock:
|
||||||
|
with lock:
|
||||||
|
if predicate():
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
if predicate():
|
||||||
|
return
|
||||||
|
attempt += 1
|
||||||
|
time.sleep(0.5)
|
||||||
|
if sendpings is not None:
|
||||||
|
sendpings()
|
||||||
|
if mocktime is not None:
|
||||||
|
mocktime(1)
|
||||||
|
|
||||||
|
# Print the cause of the timeout
|
||||||
|
assert_greater_than(attempts, attempt)
|
||||||
|
assert_greater_than(timeout, time.time())
|
||||||
|
raise RuntimeError('Unreachable')
|
||||||
|
|
||||||
|
# RPC/P2P connection constants and functions
|
||||||
|
############################################
|
||||||
|
|
||||||
|
# The maximum number of nodes a single test can spawn
|
||||||
|
MAX_NODES = 8
|
||||||
|
# Don't assign rpc or p2p ports lower than this
|
||||||
|
PORT_MIN = 11000
|
||||||
|
# The number of ports to "reserve" for p2p and rpc, each
|
||||||
|
PORT_RANGE = 5000
|
||||||
|
|
||||||
|
class PortSeed:
|
||||||
|
# Must be initialized with a unique integer for each process
|
||||||
|
n = None
|
||||||
|
|
||||||
|
def get_rpc_proxy(url, node_number, timeout=None, coveragedir=None):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
url (str): URL of the RPC server to call
|
||||||
|
node_number (int): the node number (or id) that this calls to
|
||||||
|
|
||||||
|
Kwargs:
|
||||||
|
timeout (int): HTTP timeout in seconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
AuthServiceProxy. convenience object for making RPC calls.
|
||||||
|
|
||||||
|
"""
|
||||||
|
proxy_kwargs = {}
|
||||||
|
if timeout is not None:
|
||||||
|
proxy_kwargs['timeout'] = timeout
|
||||||
|
|
||||||
|
proxy = AuthServiceProxy(url, **proxy_kwargs)
|
||||||
|
proxy.url = url # store URL on proxy for info
|
||||||
|
|
||||||
|
coverage_logfile = coverage.get_filename(
|
||||||
|
coveragedir, node_number) if coveragedir else None
|
||||||
|
|
||||||
|
return coverage.AuthServiceProxyWrapper(proxy, coverage_logfile)
|
||||||
|
|
||||||
|
def p2p_port(n):
|
||||||
|
assert(n <= MAX_NODES)
|
||||||
|
return PORT_MIN + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES)
|
||||||
|
|
||||||
|
def rpc_port(n):
|
||||||
|
return PORT_MIN + PORT_RANGE + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES)
|
||||||
|
|
||||||
|
def rpc_url(datadir, i, rpchost=None):
|
||||||
|
rpc_u, rpc_p = get_auth_cookie(datadir)
|
||||||
|
host = '127.0.0.1'
|
||||||
|
port = rpc_port(i)
|
||||||
|
if rpchost:
|
||||||
|
parts = rpchost.split(':')
|
||||||
|
if len(parts) == 2:
|
||||||
|
host, port = parts
|
||||||
|
else:
|
||||||
|
host = rpchost
|
||||||
|
return "http://%s:%s@%s:%d" % (rpc_u, rpc_p, host, int(port))
|
||||||
|
|
||||||
|
# Node functions
|
||||||
|
################
|
||||||
|
|
||||||
|
def initialize_datadir(dirname, n):
|
||||||
|
datadir = get_datadir_path(dirname, n)
|
||||||
|
if not os.path.isdir(datadir):
|
||||||
|
os.makedirs(datadir)
|
||||||
|
with open(os.path.join(datadir, "pivx.conf"), 'w', encoding='utf8') as f:
|
||||||
|
f.write("regtest=1\n")
|
||||||
|
f.write("[regtest]\n")
|
||||||
|
f.write("port=" + str(p2p_port(n)) + "\n")
|
||||||
|
f.write("rpcport=" + str(rpc_port(n)) + "\n")
|
||||||
|
f.write("server=1\n")
|
||||||
|
f.write("keypool=1\n")
|
||||||
|
f.write("discover=0\n")
|
||||||
|
f.write("listenonion=0\n")
|
||||||
|
f.write("spendzeroconfchange=1\n")
|
||||||
|
f.write("printtoconsole=0\n")
|
||||||
|
f.write("natpmp=0\n")
|
||||||
|
return datadir
|
||||||
|
|
||||||
|
def get_datadir_path(dirname, n):
|
||||||
|
return os.path.join(dirname, "node" + str(n))
|
||||||
|
|
||||||
|
def append_config(dirname, n, options):
|
||||||
|
datadir = get_datadir_path(dirname, n)
|
||||||
|
with open(os.path.join(datadir, "pivx.conf"), 'a', encoding='utf8') as f:
|
||||||
|
for option in options:
|
||||||
|
f.write(option + "\n")
|
||||||
|
|
||||||
|
def get_auth_cookie(datadir):
|
||||||
|
user = None
|
||||||
|
password = None
|
||||||
|
if os.path.isfile(os.path.join(datadir, "pivx.conf")):
|
||||||
|
with open(os.path.join(datadir, "pivx.conf"), 'r', encoding='utf8') as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith("rpcuser="):
|
||||||
|
assert user is None # Ensure that there is only one rpcuser line
|
||||||
|
user = line.split("=")[1].strip("\n")
|
||||||
|
if line.startswith("rpcpassword="):
|
||||||
|
assert password is None # Ensure that there is only one rpcpassword line
|
||||||
|
password = line.split("=")[1].strip("\n")
|
||||||
|
if os.path.isfile(os.path.join(datadir, "regtest", ".cookie")):
|
||||||
|
with open(os.path.join(datadir, "regtest", ".cookie"), 'r', encoding="utf8") as f:
|
||||||
|
userpass = f.read()
|
||||||
|
split_userpass = userpass.split(':')
|
||||||
|
user = split_userpass[0]
|
||||||
|
password = split_userpass[1]
|
||||||
|
if user is None or password is None:
|
||||||
|
raise ValueError("No RPC credentials")
|
||||||
|
return user, password
|
||||||
|
|
||||||
|
# If a cookie file exists in the given datadir, delete it.
|
||||||
|
def delete_cookie_file(datadir):
|
||||||
|
if os.path.isfile(os.path.join(datadir, "regtest", ".cookie")):
|
||||||
|
logger.debug("Deleting leftover cookie file")
|
||||||
|
os.remove(os.path.join(datadir, "regtest", ".cookie"))
|
||||||
|
|
||||||
|
def get_bip9_status(node, key):
|
||||||
|
info = node.getblockchaininfo()
|
||||||
|
return info['bip9_softforks'][key]
|
||||||
|
|
||||||
|
def set_node_times(nodes, t):
|
||||||
|
for node in nodes:
|
||||||
|
node.setmocktime(t)
|
||||||
|
|
||||||
|
def disconnect_nodes(from_connection, node_num):
|
||||||
|
for addr in [peer['addr'] for peer in from_connection.getpeerinfo() if "testnode%d" % node_num in peer['subver']]:
|
||||||
|
try:
|
||||||
|
from_connection.disconnectnode(addr)
|
||||||
|
except JSONRPCException as e:
|
||||||
|
# If this node is disconnected between calculating the peer id
|
||||||
|
# and issuing the disconnect, don't worry about it.
|
||||||
|
# This avoids a race condition if we're mass-disconnecting peers.
|
||||||
|
if e.error['code'] != -29: # RPC_CLIENT_NODE_NOT_CONNECTED
|
||||||
|
raise
|
||||||
|
|
||||||
|
# wait to disconnect
|
||||||
|
wait_until(lambda: [peer['addr'] for peer in from_connection.getpeerinfo() if "testnode%d" % node_num in peer['subver']] == [], timeout=5)
|
||||||
|
|
||||||
|
def connect_nodes(from_connection, node_num):
|
||||||
|
ip_port = "127.0.0.1:" + str(p2p_port(node_num))
|
||||||
|
from_connection.addnode(ip_port, "onetry")
|
||||||
|
# poll until version handshake complete to avoid race conditions
|
||||||
|
# with transaction relaying
|
||||||
|
wait_until(lambda: all(peer['version'] != 0 for peer in from_connection.getpeerinfo()))
|
||||||
|
|
||||||
|
def connect_nodes_clique(nodes):
|
||||||
|
l = len(nodes)
|
||||||
|
for a in range(l):
|
||||||
|
for b in range(a, l):
|
||||||
|
connect_nodes(nodes[a], b)
|
||||||
|
connect_nodes(nodes[b], a)
|
||||||
|
|
||||||
|
# Transaction/Block functions
|
||||||
|
#############################
|
||||||
|
|
||||||
|
def find_output(node, txid, amount):
|
||||||
|
"""
|
||||||
|
Return index to output of txid with value amount
|
||||||
|
Raises exception if there is none.
|
||||||
|
"""
|
||||||
|
txdata = node.getrawtransaction(txid, 1)
|
||||||
|
for i in range(len(txdata["vout"])):
|
||||||
|
if txdata["vout"][i]["value"] == amount:
|
||||||
|
return i
|
||||||
|
raise RuntimeError("find_output txid %s : %s not found" % (txid, str(amount)))
|
||||||
|
|
||||||
|
def gather_inputs(from_node, amount_needed, confirmations_required=1):
|
||||||
|
"""
|
||||||
|
Return a random set of unspent txouts that are enough to pay amount_needed
|
||||||
|
"""
|
||||||
|
assert(confirmations_required >= 0)
|
||||||
|
utxo = from_node.listunspent(confirmations_required)
|
||||||
|
random.shuffle(utxo)
|
||||||
|
inputs = []
|
||||||
|
total_in = Decimal("0.00000000")
|
||||||
|
while total_in < amount_needed and len(utxo) > 0:
|
||||||
|
t = utxo.pop()
|
||||||
|
total_in += t["amount"]
|
||||||
|
inputs.append({"txid": t["txid"], "vout": t["vout"], "address": t["address"]})
|
||||||
|
if total_in < amount_needed:
|
||||||
|
raise RuntimeError("Insufficient funds: need %d, have %d" % (amount_needed, total_in))
|
||||||
|
return (total_in, inputs)
|
||||||
|
|
||||||
|
def make_change(from_node, amount_in, amount_out, fee):
|
||||||
|
"""
|
||||||
|
Create change output(s), return them
|
||||||
|
"""
|
||||||
|
outputs = {}
|
||||||
|
amount = amount_out + fee
|
||||||
|
change = amount_in - amount
|
||||||
|
if change > amount * 2:
|
||||||
|
# Create an extra change output to break up big inputs
|
||||||
|
change_address = from_node.getnewaddress()
|
||||||
|
# Split change in two, being careful of rounding:
|
||||||
|
outputs[change_address] = Decimal(change / 2).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN)
|
||||||
|
change = amount_in - amount - outputs[change_address]
|
||||||
|
if change > 0:
|
||||||
|
outputs[from_node.getnewaddress()] = change
|
||||||
|
return outputs
|
||||||
|
|
||||||
|
def random_transaction(nodes, amount, min_fee, fee_increment, fee_variants):
|
||||||
|
"""
|
||||||
|
Create a random transaction.
|
||||||
|
Returns (txid, hex-encoded-transaction-data, fee)
|
||||||
|
"""
|
||||||
|
from_node = random.choice(nodes)
|
||||||
|
to_node = random.choice(nodes)
|
||||||
|
fee = min_fee + fee_increment * random.randint(0, fee_variants)
|
||||||
|
|
||||||
|
(total_in, inputs) = gather_inputs(from_node, amount + fee)
|
||||||
|
outputs = make_change(from_node, total_in, amount, fee)
|
||||||
|
outputs[to_node.getnewaddress()] = float(amount)
|
||||||
|
|
||||||
|
rawtx = from_node.createrawtransaction(inputs, outputs)
|
||||||
|
signresult = from_node.signrawtransaction(rawtx)
|
||||||
|
txid = from_node.sendrawtransaction(signresult["hex"], True)
|
||||||
|
|
||||||
|
return (txid, signresult["hex"], fee)
|
||||||
|
|
||||||
|
# Helper to create at least "count" utxos
|
||||||
|
# Pass in a fee that is sufficient for relay and mining new transactions.
|
||||||
|
def create_confirmed_utxos(fee, node, count):
|
||||||
|
to_generate = int(0.5 * count) + 101
|
||||||
|
while to_generate > 0:
|
||||||
|
node.generate(min(25, to_generate))
|
||||||
|
to_generate -= 25
|
||||||
|
utxos = node.listunspent()
|
||||||
|
iterations = count - len(utxos)
|
||||||
|
addr1 = node.getnewaddress()
|
||||||
|
addr2 = node.getnewaddress()
|
||||||
|
if iterations <= 0:
|
||||||
|
return utxos
|
||||||
|
for i in range(iterations):
|
||||||
|
t = utxos.pop()
|
||||||
|
inputs = []
|
||||||
|
inputs.append({"txid": t["txid"], "vout": t["vout"]})
|
||||||
|
outputs = {}
|
||||||
|
send_value = t['amount'] - fee
|
||||||
|
outputs[addr1] = float(satoshi_round(send_value / 2))
|
||||||
|
outputs[addr2] = float(satoshi_round(send_value / 2))
|
||||||
|
raw_tx = node.createrawtransaction(inputs, outputs)
|
||||||
|
signed_tx = node.signrawtransaction(raw_tx)["hex"]
|
||||||
|
node.sendrawtransaction(signed_tx)
|
||||||
|
|
||||||
|
while (node.getmempoolinfo()['size'] > 0):
|
||||||
|
node.generate(1)
|
||||||
|
|
||||||
|
utxos = node.listunspent()
|
||||||
|
assert(len(utxos) >= count)
|
||||||
|
return utxos
|
||||||
|
|
||||||
|
# Create large OP_RETURN txouts that can be appended to a transaction
|
||||||
|
# to make it large (helper for constructing large transactions).
|
||||||
|
def gen_return_txouts():
|
||||||
|
# Some pre-processing to create a bunch of OP_RETURN txouts to insert into transactions we create
|
||||||
|
# So we have big transactions (and therefore can't fit very many into each block)
|
||||||
|
# create one script_pubkey
|
||||||
|
script_pubkey = "6a4d0200" # OP_RETURN OP_PUSH2 512 bytes
|
||||||
|
for i in range(512):
|
||||||
|
script_pubkey = script_pubkey + "01"
|
||||||
|
# concatenate 128 txouts of above script_pubkey which we'll insert before the txout for change
|
||||||
|
txouts = "81"
|
||||||
|
for k in range(128):
|
||||||
|
# add txout value
|
||||||
|
txouts = txouts + "0000000000000000"
|
||||||
|
# add length of script_pubkey
|
||||||
|
txouts = txouts + "fd0402"
|
||||||
|
# add script_pubkey
|
||||||
|
txouts = txouts + script_pubkey
|
||||||
|
return txouts
|
||||||
|
|
||||||
|
def create_tx(node, coinbase, to_address, amount):
|
||||||
|
inputs = [{"txid": coinbase, "vout": 0}]
|
||||||
|
outputs = {to_address: amount}
|
||||||
|
rawtx = node.createrawtransaction(inputs, outputs)
|
||||||
|
signresult = node.signrawtransaction(rawtx)
|
||||||
|
assert_equal(signresult["complete"], True)
|
||||||
|
return signresult["hex"]
|
||||||
|
|
||||||
|
# Create a spend of each passed-in utxo, splicing in "txouts" to each raw
|
||||||
|
# transaction to make it large. See gen_return_txouts() above.
|
||||||
|
def create_lots_of_big_transactions(node, txouts, utxos, num, fee):
|
||||||
|
addr = node.getnewaddress()
|
||||||
|
txids = []
|
||||||
|
for _ in range(num):
|
||||||
|
t = utxos.pop()
|
||||||
|
inputs = [{"txid": t["txid"], "vout": t["vout"]}]
|
||||||
|
outputs = {}
|
||||||
|
change = t['amount'] - fee
|
||||||
|
outputs[addr] = float(satoshi_round(change))
|
||||||
|
rawtx = node.createrawtransaction(inputs, outputs)
|
||||||
|
newtx = rawtx[0:92]
|
||||||
|
newtx = newtx + txouts
|
||||||
|
newtx = newtx + rawtx[94:]
|
||||||
|
signresult = node.signrawtransaction(newtx, None, None, "NONE")
|
||||||
|
txid = node.sendrawtransaction(signresult["hex"], True)
|
||||||
|
txids.append(txid)
|
||||||
|
return txids
|
||||||
|
|
||||||
|
def mine_large_block(node, utxos=None):
|
||||||
|
# generate a 66k transaction,
|
||||||
|
# and 14 of them is close to the 1MB block limit
|
||||||
|
num = 14
|
||||||
|
txouts = gen_return_txouts()
|
||||||
|
utxos = utxos if utxos is not None else []
|
||||||
|
if len(utxos) < num:
|
||||||
|
utxos.clear()
|
||||||
|
utxos.extend(node.listunspent())
|
||||||
|
fee = 100 * node.getnetworkinfo()["relayfee"]
|
||||||
|
create_lots_of_big_transactions(node, txouts, utxos, num, fee=fee)
|
||||||
|
node.generate(1)
|
||||||
|
|
||||||
|
def find_vout_for_address(node, txid, addr):
|
||||||
|
"""
|
||||||
|
Locate the vout index of the given transaction sending to the
|
||||||
|
given address. Raises runtime error exception if not found.
|
||||||
|
"""
|
||||||
|
tx = node.getrawtransaction(txid, True)
|
||||||
|
for i in range(len(tx["vout"])):
|
||||||
|
if any([addr == a for a in tx["vout"][i]["scriptPubKey"]["addresses"]]):
|
||||||
|
return i
|
||||||
|
raise RuntimeError("Vout not found for address: txid=%s, addr=%s" % (txid, addr))
|
||||||
|
|
||||||
|
# PIVX specific utils
|
||||||
|
DEFAULT_FEE = 0.01
|
||||||
|
SPORK_ACTIVATION_TIME = 1563253447
|
||||||
|
SPORK_DEACTIVATION_TIME = 4070908800
|
||||||
|
|
||||||
|
def DecimalAmt(x):
|
||||||
|
"""Return Decimal from float for equality checks against rpc outputs"""
|
||||||
|
return Decimal("{:0.8f}".format(x))
|
||||||
|
|
||||||
|
# Find a coinstake/coinbase address on the node, filtering by the number of UTXOs it has.
|
||||||
|
# If no filter is provided, returns the coinstake/coinbase address on the node containing
|
||||||
|
# the greatest number of spendable UTXOs.
|
||||||
|
# The default cached chain has one address per coinbase output.
|
||||||
|
def get_coinstake_address(node, expected_utxos=None):
|
||||||
|
addrs = [utxo['address'] for utxo in node.listunspent() if utxo['generated']]
|
||||||
|
assert(len(set(addrs)) > 0)
|
||||||
|
|
||||||
|
if expected_utxos is None:
|
||||||
|
addrs = [(addrs.count(a), a) for a in set(addrs)]
|
||||||
|
return sorted(addrs, reverse=True)[0][1]
|
||||||
|
|
||||||
|
addrs = [a for a in set(addrs) if addrs.count(a) == expected_utxos]
|
||||||
|
assert(len(addrs) > 0)
|
||||||
|
return addrs[0]
|
||||||
|
|
||||||
|
# Deterministic masternodes
|
||||||
|
def is_coin_locked_by(node, outpoint):
|
||||||
|
return outpoint.to_json() in node.listlockunspent()
|
||||||
|
|
||||||
|
def get_collateral_vout(json_tx):
|
||||||
|
funding_txidn = -1
|
||||||
|
for o in json_tx["vout"]:
|
||||||
|
if o["value"] == Decimal('100'):
|
||||||
|
funding_txidn = o["n"]
|
||||||
|
break
|
||||||
|
assert_greater_than(funding_txidn, -1)
|
||||||
|
return funding_txidn
|
||||||
|
|
||||||
|
# owner and voting keys are created from controller node.
|
||||||
|
# operator keys are created, if operator_keys is None.
|
||||||
|
def create_new_dmn(idx, controller, payout_addr, operator_keys):
|
||||||
|
port = p2p_port(idx) if idx <= MAX_NODES else p2p_port(MAX_NODES) + (idx - MAX_NODES)
|
||||||
|
ipport = "127.0.0.1:" + str(port)
|
||||||
|
owner_addr = controller.getnewaddress("mnowner-%d" % idx)
|
||||||
|
voting_addr = controller.getnewaddress("mnvoting-%d" % idx)
|
||||||
|
if operator_keys is None:
|
||||||
|
bls_keypair = controller.generateblskeypair()
|
||||||
|
operator_pk = bls_keypair["public"]
|
||||||
|
operator_sk = bls_keypair["secret"]
|
||||||
|
else:
|
||||||
|
operator_pk = operator_keys[0]
|
||||||
|
operator_sk = operator_keys[1]
|
||||||
|
return messages.Masternode(idx, owner_addr, operator_pk, voting_addr, ipport, payout_addr, operator_sk)
|
||||||
|
|
||||||
|
def spend_mn_collateral(spender, dmn):
|
||||||
|
inputs = [dmn.collateral.to_json()]
|
||||||
|
outputs = {spender.getnewaddress(): Decimal('99.99')}
|
||||||
|
sig_res = spender.signrawtransaction(spender.createrawtransaction(inputs, outputs))
|
||||||
|
assert_equal(sig_res['complete'], True)
|
||||||
|
return spender.sendrawtransaction(sig_res['hex'])
|
||||||
94
basicswap/interface/dash.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2022 tecnovert
|
||||||
|
# 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.address import decodeAddress
|
||||||
|
from mnemonic import Mnemonic
|
||||||
|
from basicswap.contrib.test_framework.script import (
|
||||||
|
CScript,
|
||||||
|
OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DASHInterface(BTCInterface):
|
||||||
|
@staticmethod
|
||||||
|
def coin_type():
|
||||||
|
return Coins.DASH
|
||||||
|
|
||||||
|
def __init__(self, coin_settings, network, swap_client=None):
|
||||||
|
super().__init__(coin_settings, network, swap_client)
|
||||||
|
self._wallet_passphrase = ''
|
||||||
|
self._have_checked_seed = False
|
||||||
|
|
||||||
|
def seedToMnemonic(self, key: bytes) -> str:
|
||||||
|
return Mnemonic('english').to_mnemonic(key)
|
||||||
|
|
||||||
|
def initialiseWallet(self, key: bytes):
|
||||||
|
words = self.seedToMnemonic(key)
|
||||||
|
|
||||||
|
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:
|
||||||
|
return decodeAddress(address)[1:]
|
||||||
|
|
||||||
|
def checkExpectedSeed(self, key_hash: str):
|
||||||
|
try:
|
||||||
|
rv = self.rpc_wallet('dumphdinfo')
|
||||||
|
entropy = Mnemonic('english').to_entropy(rv['mnemonic'].split(' '))
|
||||||
|
entropy_hash = self.getAddressHashFromKey(entropy)[::-1].hex()
|
||||||
|
self._have_checked_seed = True
|
||||||
|
return entropy_hash == key_hash
|
||||||
|
except Exception as e:
|
||||||
|
self._log.warning('checkExpectedSeed failed: {}'.format(str(e)))
|
||||||
|
return False
|
||||||
|
|
||||||
|
def withdrawCoin(self, value, addr_to, subfee):
|
||||||
|
params = [addr_to, value, '', '', subfee, False, False, self._conf_target]
|
||||||
|
return self.rpc_wallet('sendtoaddress', params)
|
||||||
|
|
||||||
|
def getSpendableBalance(self) -> int:
|
||||||
|
return self.make_int(self.rpc_wallet('getwalletinfo')['balance'])
|
||||||
|
|
||||||
|
def getScriptForPubkeyHash(self, pkh: bytes) -> bytearray:
|
||||||
|
# Return P2PKH
|
||||||
|
return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG])
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def findTxnByHash(self, txid_hex: str):
|
||||||
|
# Only works for wallet txns
|
||||||
|
try:
|
||||||
|
rv = self.rpc_wallet('gettransaction', [txid_hex])
|
||||||
|
except Exception as ex:
|
||||||
|
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
|
||||||
|
return None
|
||||||
|
if 'confirmations' in rv and rv['confirmations'] >= self.blocks_confirmed:
|
||||||
|
block_height = self.getBlockHeader(rv['blockhash'])['height']
|
||||||
|
return {'txid': txid_hex, 'amount': 0, 'height': block_height}
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def unlockWallet(self, password: str):
|
||||||
|
super().unlockWallet(password)
|
||||||
|
# Store password for initialiseWallet
|
||||||
|
self._wallet_passphrase = password
|
||||||
|
if not self._have_checked_seed:
|
||||||
|
self._sc.checkWalletSeed(self.coin_type())
|
||||||
|
|
||||||
|
def lockWallet(self):
|
||||||
|
super().lockWallet()
|
||||||
|
self._wallet_passphrase = ''
|
||||||
367
basicswap/interface/firo.py
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2022-2023 tecnovert
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
|
import random
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from .btc import BTCInterface, find_vout_for_address_from_txobj
|
||||||
|
from basicswap.util import (
|
||||||
|
i2b,
|
||||||
|
ensure,
|
||||||
|
)
|
||||||
|
from basicswap.rpc import make_rpc_func
|
||||||
|
from basicswap.util.crypto import hash160
|
||||||
|
from basicswap.util.address import decodeAddress
|
||||||
|
from basicswap.chainparams import Coins
|
||||||
|
from basicswap.interface.contrib.firo_test_framework.script import (
|
||||||
|
CScript,
|
||||||
|
OP_DUP,
|
||||||
|
OP_EQUAL,
|
||||||
|
OP_HASH160,
|
||||||
|
OP_CHECKSIG,
|
||||||
|
OP_EQUALVERIFY,
|
||||||
|
)
|
||||||
|
from basicswap.interface.contrib.firo_test_framework.mininode import (
|
||||||
|
CBlock,
|
||||||
|
FromHex,
|
||||||
|
CTransaction,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FIROInterface(BTCInterface):
|
||||||
|
@staticmethod
|
||||||
|
def coin_type():
|
||||||
|
return Coins.FIRO
|
||||||
|
|
||||||
|
def __init__(self, coin_settings, network, swap_client=None):
|
||||||
|
super(FIROInterface, self).__init__(coin_settings, network, swap_client)
|
||||||
|
# No multiwallet support
|
||||||
|
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host)
|
||||||
|
|
||||||
|
def checkWallets(self) -> int:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def getExchangeName(self, exchange_name):
|
||||||
|
return 'zcoin'
|
||||||
|
|
||||||
|
def initialiseWallet(self, key):
|
||||||
|
# load with -hdseed= parameter
|
||||||
|
pass
|
||||||
|
|
||||||
|
def getNewAddress(self, use_segwit, label='swap_receive'):
|
||||||
|
return self.rpc('getnewaddress', [label])
|
||||||
|
# addr_plain = self.rpc('getnewaddress', [label])
|
||||||
|
# return self.rpc('addwitnessaddress', [addr_plain])
|
||||||
|
|
||||||
|
def decodeAddress(self, address):
|
||||||
|
return decodeAddress(address)[1:]
|
||||||
|
|
||||||
|
def encodeSegwitAddress(self, script):
|
||||||
|
raise ValueError('TODO')
|
||||||
|
|
||||||
|
def decodeSegwitAddress(self, addr):
|
||||||
|
raise ValueError('TODO')
|
||||||
|
|
||||||
|
def isWatchOnlyAddress(self, address):
|
||||||
|
addr_info = self.rpc('validateaddress', [address])
|
||||||
|
return addr_info['iswatchonly']
|
||||||
|
|
||||||
|
def isAddressMine(self, address: str, or_watch_only: bool = False) -> bool:
|
||||||
|
addr_info = self.rpc('validateaddress', [address])
|
||||||
|
if not or_watch_only:
|
||||||
|
return addr_info['ismine']
|
||||||
|
return addr_info['ismine'] or addr_info['iswatchonly']
|
||||||
|
|
||||||
|
def getSCLockScriptAddress(self, lock_script):
|
||||||
|
lock_tx_dest = self.getScriptDest(lock_script)
|
||||||
|
address = self.encodeScriptDest(lock_tx_dest)
|
||||||
|
|
||||||
|
if not self.isAddressMine(address, or_watch_only=True):
|
||||||
|
# Expects P2WSH nested in BIP16_P2SH
|
||||||
|
ro = self.rpc('importaddress', [lock_tx_dest.hex(), 'bid lock', False, True])
|
||||||
|
addr_info = self.rpc('validateaddress', [address])
|
||||||
|
|
||||||
|
return address
|
||||||
|
|
||||||
|
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False):
|
||||||
|
# Add watchonly address and rescan if required
|
||||||
|
|
||||||
|
if not self.isAddressMine(dest_address, or_watch_only=True):
|
||||||
|
self.importWatchOnlyAddress(dest_address, 'bid')
|
||||||
|
self._log.info('Imported watch-only addr: {}'.format(dest_address))
|
||||||
|
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), rescan_from))
|
||||||
|
self.rescanBlockchainForAddress(rescan_from, dest_address)
|
||||||
|
|
||||||
|
return_txid = True if txid is None else False
|
||||||
|
if txid is None:
|
||||||
|
txns = self.rpc('listunspent', [0, 9999999, [dest_address, ]])
|
||||||
|
|
||||||
|
for tx in txns:
|
||||||
|
if self.make_int(tx['amount']) == bid_amount:
|
||||||
|
txid = bytes.fromhex(tx['txid'])
|
||||||
|
break
|
||||||
|
|
||||||
|
if txid is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
tx = self.rpc('gettransaction', [txid.hex()])
|
||||||
|
|
||||||
|
block_height = 0
|
||||||
|
if 'blockhash' in tx:
|
||||||
|
block_header = self.rpc('getblockheader', [tx['blockhash']])
|
||||||
|
block_height = block_header['height']
|
||||||
|
|
||||||
|
rv = {
|
||||||
|
'depth': 0 if 'confirmations' not in tx else tx['confirmations'],
|
||||||
|
'height': block_height}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._log.debug('getLockTxHeight gettransaction failed: %s, %s', txid.hex(), str(e))
|
||||||
|
return None
|
||||||
|
|
||||||
|
if find_index:
|
||||||
|
tx_obj = self.rpc('decoderawtransaction', [tx['hex']])
|
||||||
|
rv['index'] = find_vout_for_address_from_txobj(tx_obj, dest_address)
|
||||||
|
|
||||||
|
if return_txid:
|
||||||
|
rv['txid'] = txid.hex()
|
||||||
|
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def createSCLockTx(self, value: int, script: bytearray, vkbv: bytes = None) -> bytes:
|
||||||
|
tx = CTransaction()
|
||||||
|
tx.nVersion = self.txVersion()
|
||||||
|
tx.vout.append(self.txoType()(value, self.getScriptDest(script)))
|
||||||
|
|
||||||
|
return tx.serialize()
|
||||||
|
|
||||||
|
def fundSCLockTx(self, tx_bytes, feerate, vkbv=None):
|
||||||
|
return self.fundTx(tx_bytes, feerate)
|
||||||
|
|
||||||
|
def signTxWithWallet(self, tx):
|
||||||
|
rv = self.rpc('signrawtransaction', [tx.hex()])
|
||||||
|
return bytes.fromhex(rv['hex'])
|
||||||
|
|
||||||
|
def createRawFundedTransaction(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)
|
||||||
|
self._log.debug(f'Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}')
|
||||||
|
options = {
|
||||||
|
'lockUnspents': lock_unspents,
|
||||||
|
'feeRate': fee_rate,
|
||||||
|
}
|
||||||
|
if sub_fee:
|
||||||
|
options['subtractFeeFromOutputs'] = [0,]
|
||||||
|
return self.rpc('fundrawtransaction', [txn, options])['hex']
|
||||||
|
|
||||||
|
def createRawSignedTransaction(self, addr_to, amount) -> str:
|
||||||
|
txn_funded = self.createRawFundedTransaction(addr_to, amount)
|
||||||
|
return self.rpc('signrawtransaction', [txn_funded])['hex']
|
||||||
|
|
||||||
|
def getScriptForPubkeyHash(self, pkh: bytes) -> bytearray:
|
||||||
|
# Return P2PKH
|
||||||
|
return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG])
|
||||||
|
|
||||||
|
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 getSeedHash(self, seed: bytes) -> bytes:
|
||||||
|
return hash160(seed)[::-1]
|
||||||
|
|
||||||
|
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 getDestForScriptHash(self, script_hash):
|
||||||
|
assert len(script_hash) == 20
|
||||||
|
return CScript([OP_HASH160, script_hash, OP_EQUAL])
|
||||||
|
|
||||||
|
def withdrawCoin(self, value, addr_to, subfee):
|
||||||
|
params = [addr_to, value, '', '', subfee]
|
||||||
|
return self.rpc('sendtoaddress', params)
|
||||||
|
|
||||||
|
def getWalletSeedID(self):
|
||||||
|
return self.rpc('getwalletinfo')['hdmasterkeyid']
|
||||||
|
|
||||||
|
def getSpendableBalance(self) -> int:
|
||||||
|
return self.make_int(self.rpc('getwalletinfo')['balance'])
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def signTxWithKey(self, tx: bytes, key: bytes) -> bytes:
|
||||||
|
key_wif = self.encodeKey(key)
|
||||||
|
rv = self.rpc('signrawtransaction', [tx.hex(), [], [key_wif, ]])
|
||||||
|
return bytes.fromhex(rv['hex'])
|
||||||
|
|
||||||
|
def findTxnByHash(self, txid_hex: str):
|
||||||
|
# Only works for wallet txns
|
||||||
|
try:
|
||||||
|
rv = self.rpc('gettransaction', [txid_hex])
|
||||||
|
except Exception as ex:
|
||||||
|
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
|
||||||
|
return None
|
||||||
|
if 'confirmations' in rv and rv['confirmations'] >= self.blocks_confirmed:
|
||||||
|
block_height = self.getBlockHeader(rv['blockhash'])['height']
|
||||||
|
return {'txid': txid_hex, 'amount': 0, 'height': block_height}
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getProofOfFunds(self, amount_for, extra_commit_bytes):
|
||||||
|
# TODO: Lock unspent and use same output/s to fund bid
|
||||||
|
|
||||||
|
unspents_by_addr = dict()
|
||||||
|
unspents = self.rpc('listunspent')
|
||||||
|
for u in unspents:
|
||||||
|
if u['spendable'] is not True:
|
||||||
|
continue
|
||||||
|
if u['address'] not in unspents_by_addr:
|
||||||
|
unspents_by_addr[u['address']] = {'total': 0, 'utxos': []}
|
||||||
|
utxo_amount: int = self.make_int(u['amount'], r=1)
|
||||||
|
unspents_by_addr[u['address']]['total'] += utxo_amount
|
||||||
|
unspents_by_addr[u['address']]['utxos'].append((utxo_amount, u['txid'], u['vout']))
|
||||||
|
|
||||||
|
max_utxos: int = 4
|
||||||
|
|
||||||
|
viable_addrs = []
|
||||||
|
for addr, data in unspents_by_addr.items():
|
||||||
|
if data['total'] >= amount_for:
|
||||||
|
# Sort from largest to smallest amount
|
||||||
|
sorted_utxos = sorted(data['utxos'], key=lambda x: x[0])
|
||||||
|
|
||||||
|
# Max outputs required to reach amount_for
|
||||||
|
utxos_req: int = 0
|
||||||
|
sum_value: int = 0
|
||||||
|
for utxo in sorted_utxos:
|
||||||
|
sum_value += utxo[0]
|
||||||
|
utxos_req += 1
|
||||||
|
if sum_value >= amount_for:
|
||||||
|
break
|
||||||
|
|
||||||
|
if utxos_req <= max_utxos:
|
||||||
|
viable_addrs.append(addr)
|
||||||
|
continue
|
||||||
|
|
||||||
|
ensure(len(viable_addrs) > 0, 'Could not find address with enough funds for proof')
|
||||||
|
|
||||||
|
sign_for_addr: str = random.choice(viable_addrs)
|
||||||
|
self._log.debug('sign_for_addr %s', sign_for_addr)
|
||||||
|
|
||||||
|
prove_utxos = []
|
||||||
|
sorted_utxos = sorted(unspents_by_addr[sign_for_addr]['utxos'], key=lambda x: x[0])
|
||||||
|
|
||||||
|
hasher = hashlib.sha256()
|
||||||
|
|
||||||
|
sum_value: int = 0
|
||||||
|
for utxo in sorted_utxos:
|
||||||
|
sum_value += utxo[0]
|
||||||
|
outpoint = (bytes.fromhex(utxo[1]), utxo[2])
|
||||||
|
prove_utxos.append(outpoint)
|
||||||
|
hasher.update(outpoint[0])
|
||||||
|
hasher.update(outpoint[1].to_bytes(2, 'big'))
|
||||||
|
if sum_value >= amount_for:
|
||||||
|
break
|
||||||
|
utxos_hash = hasher.digest()
|
||||||
|
|
||||||
|
self._log.debug('sign_for_addr %s', sign_for_addr)
|
||||||
|
|
||||||
|
if self.using_segwit(): # TODO: Use isSegwitAddress when scantxoutset can use combo
|
||||||
|
# 'Address does not refer to key' for non p2pkh
|
||||||
|
pkh = self.decodeAddress(sign_for_addr)
|
||||||
|
sign_for_addr = self.pkh_to_address(pkh)
|
||||||
|
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()])
|
||||||
|
|
||||||
|
return (sign_for_addr, signature, prove_utxos)
|
||||||
|
|
||||||
|
def verifyProofOfFunds(self, address, signature, utxos, extra_commit_bytes):
|
||||||
|
hasher = hashlib.sha256()
|
||||||
|
sum_value: int = 0
|
||||||
|
for outpoint in utxos:
|
||||||
|
hasher.update(outpoint[0])
|
||||||
|
hasher.update(outpoint[1].to_bytes(2, 'big'))
|
||||||
|
utxos_hash = hasher.digest()
|
||||||
|
|
||||||
|
passed = self.verifyMessage(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():
|
||||||
|
address = self.encodeSegwitAddress(decodeAddress(address)[1:])
|
||||||
|
|
||||||
|
sum_value: int = 0
|
||||||
|
for outpoint in utxos:
|
||||||
|
txout = self.rpc('gettxout', [outpoint[0].hex(), outpoint[1]])
|
||||||
|
sum_value += self.make_int(txout['value'])
|
||||||
|
|
||||||
|
return sum_value
|
||||||
|
|
||||||
|
def rescanBlockchainForAddress(self, height_start: int, addr_find: str):
|
||||||
|
# Very ugly workaround for missing `rescanblockchain` rpc command
|
||||||
|
|
||||||
|
chain_blocks: int = self.getChainHeight()
|
||||||
|
|
||||||
|
current_height: int = chain_blocks
|
||||||
|
block_hash = self.rpc('getblockhash', [current_height])
|
||||||
|
|
||||||
|
script_hash: bytes = self.decodeAddress(addr_find)
|
||||||
|
find_scriptPubKey = self.getDestForScriptHash(script_hash)
|
||||||
|
|
||||||
|
while current_height > height_start:
|
||||||
|
block_hash = self.rpc('getblockhash', [current_height])
|
||||||
|
|
||||||
|
block = self.rpc('getblock', [block_hash, False])
|
||||||
|
decoded_block = CBlock()
|
||||||
|
decoded_block = FromHex(decoded_block, block)
|
||||||
|
for tx in decoded_block.vtx:
|
||||||
|
for txo in tx.vout:
|
||||||
|
if txo.scriptPubKey == find_scriptPubKey:
|
||||||
|
tx.rehash()
|
||||||
|
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('rescanblockchain hack invalidateblock {}'.format(block_hash))
|
||||||
|
self.rpc('invalidateblock', [block_hash])
|
||||||
|
self.rpc('reconsiderblock', [block_hash])
|
||||||
|
return
|
||||||
|
current_height -= 1
|
||||||
|
|
||||||
|
def getBlockWithTxns(self, block_hash):
|
||||||
|
# TODO: Bypass decoderawtransaction and getblockheader
|
||||||
|
block = self.rpc('getblock', [block_hash, False])
|
||||||
|
block_header = self.rpc('getblockheader', [block_hash])
|
||||||
|
decoded_block = CBlock()
|
||||||
|
decoded_block = FromHex(decoded_block, block)
|
||||||
|
|
||||||
|
tx_rv = []
|
||||||
|
for tx in decoded_block.vtx:
|
||||||
|
tx_hex = tx.serialize_with_witness().hex()
|
||||||
|
tx_dec = self.rpc('decoderawtransaction', [tx_hex])
|
||||||
|
if 'hex' not in tx_dec:
|
||||||
|
tx_dec['hex'] = tx_hex
|
||||||
|
|
||||||
|
tx_rv.append(tx_dec)
|
||||||
|
|
||||||
|
block_rv = {
|
||||||
|
'hash': block_hash,
|
||||||
|
'tx': tx_rv,
|
||||||
|
'confirmations': block_header['confirmations'],
|
||||||
|
'height': block_header['height'],
|
||||||
|
'version': block_header['version'],
|
||||||
|
'merkleroot': block_header['merkleroot'],
|
||||||
|
}
|
||||||
|
|
||||||
|
return block_rv
|
||||||
@@ -1,15 +1,122 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2020 tecnovert
|
# Copyright (c) 2020-2023 tecnovert
|
||||||
# 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.
|
||||||
|
|
||||||
from .btc import BTCInterface
|
from .btc import BTCInterface
|
||||||
from basicswap.chainparams import Coins
|
from basicswap.rpc import make_rpc_func
|
||||||
|
from basicswap.chainparams import Coins, chainparams
|
||||||
|
|
||||||
|
|
||||||
class LTCInterface(BTCInterface):
|
class LTCInterface(BTCInterface):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def coin_type():
|
def coin_type():
|
||||||
return Coins.LTC
|
return Coins.LTC
|
||||||
|
|
||||||
|
def __init__(self, coin_settings, network, swap_client=None):
|
||||||
|
super(LTCInterface, self).__init__(coin_settings, network, swap_client)
|
||||||
|
self._rpc_wallet_mweb = '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:
|
||||||
|
return self.rpc_wallet_mweb('getnewaddress', [label, 'mweb'])
|
||||||
|
|
||||||
|
def getNewStealthAddress(self, label=''):
|
||||||
|
return self.getNewMwebAddress(False, label)
|
||||||
|
|
||||||
|
def withdrawCoin(self, value, type_from: str, addr_to: str, subfee: bool) -> str:
|
||||||
|
params = [addr_to, value, '', '', subfee, True, self._conf_target]
|
||||||
|
if type_from == 'mweb':
|
||||||
|
return self.rpc_wallet_mweb('sendtoaddress', params)
|
||||||
|
return self.rpc_wallet('sendtoaddress', params)
|
||||||
|
|
||||||
|
def getWalletInfo(self):
|
||||||
|
rv = super(LTCInterface, self).getWalletInfo()
|
||||||
|
|
||||||
|
mweb_info = self.rpc_wallet_mweb('getwalletinfo')
|
||||||
|
rv['mweb_balance'] = mweb_info['balance']
|
||||||
|
rv['mweb_unconfirmed'] = mweb_info['unconfirmed_balance']
|
||||||
|
rv['mweb_immature'] = mweb_info['immature_balance']
|
||||||
|
|
||||||
|
# Add unconfirmed mweb -> plain txns to the unconfirmed_balance
|
||||||
|
txns = self.rpc_wallet('listtransactions')
|
||||||
|
for tx in reversed(txns):
|
||||||
|
amount: float = tx.get('amount', 0.0)
|
||||||
|
if tx['confirmations'] == 0 and tx.get('mweb_out', None) and amount > 0.0:
|
||||||
|
rv['unconfirmed_balance'] += amount
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
class LTCInterfaceMWEB(LTCInterface):
|
||||||
|
@staticmethod
|
||||||
|
def coin_type():
|
||||||
|
return Coins.LTC_MWEB
|
||||||
|
|
||||||
|
def __init__(self, coin_settings, network, swap_client=None):
|
||||||
|
super(LTCInterfaceMWEB, self).__init__(coin_settings, network, swap_client)
|
||||||
|
self._rpc_wallet = 'mweb'
|
||||||
|
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet)
|
||||||
|
|
||||||
|
def chainparams(self):
|
||||||
|
return chainparams[Coins.LTC]
|
||||||
|
|
||||||
|
def chainparams_network(self):
|
||||||
|
return chainparams[Coins.LTC][self._network]
|
||||||
|
|
||||||
|
def coin_name(self) -> str:
|
||||||
|
coin_chainparams = chainparams[Coins.LTC]
|
||||||
|
if coin_chainparams.get('use_ticker_as_name', False):
|
||||||
|
return coin_chainparams['ticker'] + ' MWEB'
|
||||||
|
return coin_chainparams['name'].capitalize() + ' MWEB'
|
||||||
|
|
||||||
|
def ticker(self) -> str:
|
||||||
|
ticker = chainparams[Coins.LTC]['ticker']
|
||||||
|
if self._network == 'testnet':
|
||||||
|
ticker = 't' + ticker
|
||||||
|
elif self._network == 'regtest':
|
||||||
|
ticker = 'rt' + ticker
|
||||||
|
return ticker + '_MWEB'
|
||||||
|
|
||||||
|
def getNewAddress(self, use_segwit=False, label='swap_receive') -> str:
|
||||||
|
return self.getNewMwebAddress()
|
||||||
|
|
||||||
|
def has_mweb_wallet(self) -> bool:
|
||||||
|
return 'mweb' in self.rpc('listwallets')
|
||||||
|
|
||||||
|
def init_wallet(self, password=None):
|
||||||
|
# If system is encrypted mweb wallet will be created at first unlock
|
||||||
|
|
||||||
|
self._log.info('init_wallet - {}'.format(self.ticker()))
|
||||||
|
|
||||||
|
self._log.info('Creating mweb wallet for {}.'.format(self.coin_name()))
|
||||||
|
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors, load_on_startup
|
||||||
|
self.rpc('createwallet', ['mweb', False, True, password, False, False, True])
|
||||||
|
|
||||||
|
if password is not None:
|
||||||
|
# Max timeout value, ~3 years
|
||||||
|
self.rpc_wallet('walletpassphrase', [password, 100000000])
|
||||||
|
|
||||||
|
if self.getWalletSeedID() == 'Not found':
|
||||||
|
self._sc.initialiseWallet(self.coin_type())
|
||||||
|
|
||||||
|
# Workaround to trigger mweb_spk_man->LoadMWEBKeychain()
|
||||||
|
self.rpc('unloadwallet', ['mweb'])
|
||||||
|
self.rpc('loadwallet', ['mweb'])
|
||||||
|
if password is not None:
|
||||||
|
self.rpc_wallet('walletpassphrase', [password, 100000000])
|
||||||
|
self.rpc_wallet('keypoolrefill')
|
||||||
|
|
||||||
|
def unlockWallet(self, password: str):
|
||||||
|
if password == '':
|
||||||
|
return
|
||||||
|
self._log.info('unlockWallet - {}'.format(self.ticker()))
|
||||||
|
|
||||||
|
if not self.has_mweb_wallet():
|
||||||
|
self.init_wallet(password)
|
||||||
|
else:
|
||||||
|
# Max timeout value, ~3 years
|
||||||
|
self.rpc_wallet('walletpassphrase', [password, 100000000])
|
||||||
|
|
||||||
|
self._sc.checkWalletSeed(self.coin_type())
|
||||||
|
|||||||
738
basicswap/interface/nav.py
Normal file
@@ -0,0 +1,738 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2023 tecnovert
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
|
import random
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from io import BytesIO
|
||||||
|
from coincurve.keys import (
|
||||||
|
PublicKey,
|
||||||
|
PrivateKey,
|
||||||
|
)
|
||||||
|
from .btc import BTCInterface, find_vout_for_address_from_txobj, findOutput
|
||||||
|
from basicswap.rpc import make_rpc_func
|
||||||
|
from basicswap.chainparams import Coins
|
||||||
|
from basicswap.interface.contrib.nav_test_framework.mininode import (
|
||||||
|
CTxIn,
|
||||||
|
CTxOut,
|
||||||
|
CBlock,
|
||||||
|
COutPoint,
|
||||||
|
CTransaction,
|
||||||
|
CTxInWitness,
|
||||||
|
FromHex,
|
||||||
|
uint256_from_str,
|
||||||
|
)
|
||||||
|
from basicswap.util.crypto import hash160
|
||||||
|
from basicswap.util.address import (
|
||||||
|
decodeWif,
|
||||||
|
pubkeyToAddress,
|
||||||
|
encodeAddress,
|
||||||
|
)
|
||||||
|
from basicswap.util import (
|
||||||
|
i2b, i2h,
|
||||||
|
ensure,
|
||||||
|
)
|
||||||
|
from basicswap.basicswap_util import (
|
||||||
|
getVoutByScriptPubKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
from basicswap.interface.contrib.nav_test_framework.script import (
|
||||||
|
CScript,
|
||||||
|
OP_0,
|
||||||
|
OP_EQUAL,
|
||||||
|
OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG,
|
||||||
|
SIGHASH_ALL,
|
||||||
|
SegwitVersion1SignatureHash,
|
||||||
|
)
|
||||||
|
from mnemonic import Mnemonic
|
||||||
|
|
||||||
|
|
||||||
|
class NAVInterface(BTCInterface):
|
||||||
|
@staticmethod
|
||||||
|
def coin_type():
|
||||||
|
return Coins.NAV
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def txVersion() -> int:
|
||||||
|
return 3
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def txoType():
|
||||||
|
return CTxOut
|
||||||
|
|
||||||
|
def __init__(self, coin_settings, network, swap_client=None):
|
||||||
|
super(NAVInterface, self).__init__(coin_settings, network, swap_client)
|
||||||
|
# No multiwallet support
|
||||||
|
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host)
|
||||||
|
|
||||||
|
def checkWallets(self) -> int:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def use_p2shp2wsh(self) -> bool:
|
||||||
|
# p2sh-p2wsh
|
||||||
|
return True
|
||||||
|
|
||||||
|
def seedToMnemonic(self, key):
|
||||||
|
return Mnemonic('english').to_mnemonic(key)
|
||||||
|
|
||||||
|
def initialiseWallet(self, key):
|
||||||
|
# load with -importmnemonic= parameter
|
||||||
|
pass
|
||||||
|
|
||||||
|
def getWalletSeedID(self):
|
||||||
|
return self.rpc('getwalletinfo')['hdmasterkeyid']
|
||||||
|
|
||||||
|
def withdrawCoin(self, value, addr_to: str, subfee: bool):
|
||||||
|
strdzeel = ''
|
||||||
|
params = [addr_to, value, '', '', strdzeel, subfee]
|
||||||
|
return self.rpc('sendtoaddress', params)
|
||||||
|
|
||||||
|
def getSpendableBalance(self) -> int:
|
||||||
|
return self.make_int(self.rpc('getwalletinfo')['balance'])
|
||||||
|
|
||||||
|
def signTxWithWallet(self, tx: bytes) -> bytes:
|
||||||
|
rv = self.rpc('signrawtransaction', [tx.hex()])
|
||||||
|
|
||||||
|
return bytes.fromhex(rv['hex'])
|
||||||
|
|
||||||
|
def checkExpectedSeed(self, key_hash: str):
|
||||||
|
try:
|
||||||
|
rv = self.rpc('dumpmnemonic')
|
||||||
|
entropy = Mnemonic('english').to_entropy(rv.split(' '))
|
||||||
|
|
||||||
|
entropy_hash = self.getAddressHashFromKey(entropy)[::-1].hex()
|
||||||
|
self._have_checked_seed = True
|
||||||
|
return entropy_hash == key_hash
|
||||||
|
except Exception as e:
|
||||||
|
self._log.warning('checkExpectedSeed failed: {}'.format(str(e)))
|
||||||
|
return False
|
||||||
|
|
||||||
|
def getScriptForP2PKH(self, pkh: bytes) -> bytearray:
|
||||||
|
# Return P2PKH
|
||||||
|
return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG])
|
||||||
|
|
||||||
|
def getScriptForPubkeyHash(self, pkh: bytes) -> bytearray:
|
||||||
|
# Return P2SH-p2wpkh
|
||||||
|
|
||||||
|
script = CScript([OP_0, pkh])
|
||||||
|
script_hash = hash160(script)
|
||||||
|
assert len(script_hash) == 20
|
||||||
|
|
||||||
|
return CScript([OP_HASH160, script_hash, OP_EQUAL])
|
||||||
|
|
||||||
|
def getInputScriptForPubkeyHash(self, pkh: bytes) -> bytearray:
|
||||||
|
script = CScript([OP_0, pkh])
|
||||||
|
return bytes((len(script),)) + script
|
||||||
|
|
||||||
|
def encodeSegwitAddress(self, pkh: bytes) -> str:
|
||||||
|
# P2SH-p2wpkh
|
||||||
|
script = CScript([OP_0, pkh])
|
||||||
|
script_hash = hash160(script)
|
||||||
|
assert len(script_hash) == 20
|
||||||
|
return encodeAddress(bytes((self.chainparams_network()['script_address'],)) + script_hash)
|
||||||
|
|
||||||
|
def encodeSegwitAddressScript(self, script: bytes) -> str:
|
||||||
|
if len(script) == 23 and script[0] == OP_HASH160 and script[1] == 20 and script[22] == OP_EQUAL:
|
||||||
|
script_hash = script[2:22]
|
||||||
|
return encodeAddress(bytes((self.chainparams_network()['script_address'],)) + script_hash)
|
||||||
|
raise ValueError('Unknown Script')
|
||||||
|
|
||||||
|
def loadTx(self, tx_bytes: bytes) -> CTransaction:
|
||||||
|
# Load tx from bytes to internal representation
|
||||||
|
tx = CTransaction()
|
||||||
|
tx.deserialize(BytesIO(tx_bytes))
|
||||||
|
return tx
|
||||||
|
|
||||||
|
def signTx(self, key_bytes: bytes, tx_bytes: bytes, input_n: int, prevout_script, prevout_value: int):
|
||||||
|
tx = self.loadTx(tx_bytes)
|
||||||
|
sig_hash = SegwitVersion1SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value)
|
||||||
|
eck = PrivateKey(key_bytes)
|
||||||
|
return eck.sign(sig_hash, hasher=None) + bytes((SIGHASH_ALL,))
|
||||||
|
|
||||||
|
def setTxSignature(self, tx_bytes: bytes, stack) -> bytes:
|
||||||
|
tx = self.loadTx(tx_bytes)
|
||||||
|
tx.wit.vtxinwit.clear()
|
||||||
|
tx.wit.vtxinwit.append(CTxInWitness())
|
||||||
|
tx.wit.vtxinwit[0].scriptWitness.stack = stack
|
||||||
|
return tx.serialize_with_witness()
|
||||||
|
|
||||||
|
def getProofOfFunds(self, amount_for, extra_commit_bytes):
|
||||||
|
# TODO: Lock unspent and use same output/s to fund bid
|
||||||
|
|
||||||
|
unspents_by_addr = dict()
|
||||||
|
unspents = self.rpc('listunspent')
|
||||||
|
for u in unspents:
|
||||||
|
if u['spendable'] is not True:
|
||||||
|
continue
|
||||||
|
if u['address'] not in unspents_by_addr:
|
||||||
|
unspents_by_addr[u['address']] = {'total': 0, 'utxos': []}
|
||||||
|
utxo_amount: int = self.make_int(u['amount'], r=1)
|
||||||
|
unspents_by_addr[u['address']]['total'] += utxo_amount
|
||||||
|
unspents_by_addr[u['address']]['utxos'].append((utxo_amount, u['txid'], u['vout']))
|
||||||
|
|
||||||
|
max_utxos: int = 4
|
||||||
|
|
||||||
|
viable_addrs = []
|
||||||
|
for addr, data in unspents_by_addr.items():
|
||||||
|
if data['total'] >= amount_for:
|
||||||
|
# Sort from largest to smallest amount
|
||||||
|
sorted_utxos = sorted(data['utxos'], key=lambda x: x[0])
|
||||||
|
|
||||||
|
# Max outputs required to reach amount_for
|
||||||
|
utxos_req: int = 0
|
||||||
|
sum_value: int = 0
|
||||||
|
for utxo in sorted_utxos:
|
||||||
|
sum_value += utxo[0]
|
||||||
|
utxos_req += 1
|
||||||
|
if sum_value >= amount_for:
|
||||||
|
break
|
||||||
|
|
||||||
|
if utxos_req <= max_utxos:
|
||||||
|
viable_addrs.append(addr)
|
||||||
|
continue
|
||||||
|
|
||||||
|
ensure(len(viable_addrs) > 0, 'Could not find address with enough funds for proof')
|
||||||
|
|
||||||
|
sign_for_addr: str = random.choice(viable_addrs)
|
||||||
|
self._log.debug('sign_for_addr %s', sign_for_addr)
|
||||||
|
|
||||||
|
prove_utxos = []
|
||||||
|
sorted_utxos = sorted(unspents_by_addr[sign_for_addr]['utxos'], key=lambda x: x[0])
|
||||||
|
|
||||||
|
hasher = hashlib.sha256()
|
||||||
|
|
||||||
|
sum_value: int = 0
|
||||||
|
for utxo in sorted_utxos:
|
||||||
|
sum_value += utxo[0]
|
||||||
|
outpoint = (bytes.fromhex(utxo[1]), utxo[2])
|
||||||
|
prove_utxos.append(outpoint)
|
||||||
|
hasher.update(outpoint[0])
|
||||||
|
hasher.update(outpoint[1].to_bytes(2, 'big'))
|
||||||
|
if sum_value >= amount_for:
|
||||||
|
break
|
||||||
|
utxos_hash = hasher.digest()
|
||||||
|
|
||||||
|
self._log.debug('sign_for_addr %s', sign_for_addr)
|
||||||
|
|
||||||
|
if self.using_segwit(): # TODO: Use isSegwitAddress when scantxoutset can use combo
|
||||||
|
# 'Address does not refer to key' for non p2pkh
|
||||||
|
addr_info = self.rpc('validateaddress', [addr, ])
|
||||||
|
if 'isscript' in addr_info and addr_info['isscript'] and 'hex' in addr_info:
|
||||||
|
pkh = bytes.fromhex(addr_info['hex'])[2:]
|
||||||
|
sign_for_addr = self.pkh_to_address(pkh)
|
||||||
|
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()])
|
||||||
|
|
||||||
|
return (sign_for_addr, signature, prove_utxos)
|
||||||
|
|
||||||
|
def verifyProofOfFunds(self, address, signature, utxos, extra_commit_bytes):
|
||||||
|
hasher = hashlib.sha256()
|
||||||
|
sum_value: int = 0
|
||||||
|
for outpoint in utxos:
|
||||||
|
hasher.update(outpoint[0])
|
||||||
|
hasher.update(outpoint[1].to_bytes(2, 'big'))
|
||||||
|
utxos_hash = hasher.digest()
|
||||||
|
|
||||||
|
passed = self.verifyMessage(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():
|
||||||
|
address = self.encodeSegwitAddress(self.decodeAddress(address)[1:])
|
||||||
|
|
||||||
|
sum_value: int = 0
|
||||||
|
for outpoint in utxos:
|
||||||
|
txout = self.rpc('gettxout', [outpoint[0].hex(), outpoint[1]])
|
||||||
|
sum_value += self.make_int(txout['value'])
|
||||||
|
|
||||||
|
return sum_value
|
||||||
|
|
||||||
|
def createRawFundedTransaction(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)
|
||||||
|
self._log.debug(f'Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}')
|
||||||
|
if sub_fee:
|
||||||
|
raise ValueError('Navcoin fundrawtransaction is missing the subtractFeeFromOutputs parameter')
|
||||||
|
# options['subtractFeeFromOutputs'] = [0,]
|
||||||
|
|
||||||
|
fee_rate = self.make_int(fee_rate, r=1)
|
||||||
|
return self.fundTx(txn, fee_rate, lock_unspents).hex()
|
||||||
|
|
||||||
|
def isAddressMine(self, address: str, or_watch_only: bool = False) -> bool:
|
||||||
|
addr_info = self.rpc('validateaddress', [address])
|
||||||
|
if not or_watch_only:
|
||||||
|
return addr_info['ismine']
|
||||||
|
return addr_info['ismine'] or addr_info['iswatchonly']
|
||||||
|
|
||||||
|
def createRawSignedTransaction(self, addr_to, amount) -> str:
|
||||||
|
txn_funded = self.createRawFundedTransaction(addr_to, amount)
|
||||||
|
return self.rpc('signrawtransaction', [txn_funded])['hex']
|
||||||
|
|
||||||
|
def getBlockchainInfo(self):
|
||||||
|
rv = self.rpc('getblockchaininfo')
|
||||||
|
synced = round(rv['verificationprogress'], 3)
|
||||||
|
if synced >= 0.997:
|
||||||
|
rv['verificationprogress'] = 1.0
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def encodeScriptDest(self, script_dest: bytes) -> str:
|
||||||
|
script_hash = script_dest[2:-1] # Extract hash from script
|
||||||
|
return self.sh_to_address(script_hash)
|
||||||
|
|
||||||
|
def encode_p2wsh(self, script: bytes) -> str:
|
||||||
|
return pubkeyToAddress(self.chainparams_network()['script_address'], script)
|
||||||
|
|
||||||
|
def find_prevout_info(self, txn_hex: str, txn_script: bytes):
|
||||||
|
txjs = self.rpc('decoderawtransaction', [txn_hex])
|
||||||
|
n = getVoutByScriptPubKey(txjs, self.getScriptDest(txn_script).hex())
|
||||||
|
|
||||||
|
return {
|
||||||
|
'txid': txjs['txid'],
|
||||||
|
'vout': n,
|
||||||
|
'scriptPubKey': txjs['vout'][n]['scriptPubKey']['hex'],
|
||||||
|
'redeemScript': txn_script.hex(),
|
||||||
|
'amount': txjs['vout'][n]['value']
|
||||||
|
}
|
||||||
|
|
||||||
|
def getNewAddress(self, use_segwit: bool, label: str = 'swap_receive') -> str:
|
||||||
|
address: str = self.rpc('getnewaddress', [label,])
|
||||||
|
if use_segwit:
|
||||||
|
return self.rpc('addwitnessaddress', [address,])
|
||||||
|
return address
|
||||||
|
|
||||||
|
def createRedeemTxn(self, prevout, output_addr: str, output_value: int, txn_script: bytes) -> str:
|
||||||
|
tx = CTransaction()
|
||||||
|
tx.nVersion = self.txVersion()
|
||||||
|
prev_txid = uint256_from_str(bytes.fromhex(prevout['txid'])[::-1])
|
||||||
|
|
||||||
|
tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout']),
|
||||||
|
scriptSig=self.getScriptScriptSig(txn_script)))
|
||||||
|
pkh = self.decodeAddress(output_addr)
|
||||||
|
script = self.getScriptForPubkeyHash(pkh)
|
||||||
|
tx.vout.append(self.txoType()(output_value, script))
|
||||||
|
tx.rehash()
|
||||||
|
return tx.serialize().hex()
|
||||||
|
|
||||||
|
def createRefundTxn(self, prevout, output_addr: str, output_value: int, locktime: int, sequence: int, txn_script: bytes) -> str:
|
||||||
|
tx = CTransaction()
|
||||||
|
tx.nVersion = self.txVersion()
|
||||||
|
tx.nLockTime = locktime
|
||||||
|
prev_txid = uint256_from_str(bytes.fromhex(prevout['txid'])[::-1])
|
||||||
|
tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout']),
|
||||||
|
nSequence=sequence,
|
||||||
|
scriptSig=self.getScriptScriptSig(txn_script)))
|
||||||
|
pkh = self.decodeAddress(output_addr)
|
||||||
|
script = self.getScriptForPubkeyHash(pkh)
|
||||||
|
tx.vout.append(self.txoType()(output_value, script))
|
||||||
|
tx.rehash()
|
||||||
|
return tx.serialize().hex()
|
||||||
|
|
||||||
|
def getTxSignature(self, tx_hex: str, prevout_data, key_wif: str) -> str:
|
||||||
|
key = decodeWif(key_wif)
|
||||||
|
redeem_script = bytes.fromhex(prevout_data['redeemScript'])
|
||||||
|
sig = self.signTx(key, bytes.fromhex(tx_hex), 0, redeem_script, self.make_int(prevout_data['amount']))
|
||||||
|
|
||||||
|
return sig.hex()
|
||||||
|
|
||||||
|
def verifyTxSig(self, tx_bytes: bytes, sig: bytes, K: bytes, input_n: int, prevout_script: bytes, prevout_value: int) -> bool:
|
||||||
|
tx = self.loadTx(tx_bytes)
|
||||||
|
sig_hash = SegwitVersion1SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value)
|
||||||
|
|
||||||
|
pubkey = PublicKey(K)
|
||||||
|
return pubkey.verify(sig[: -1], sig_hash, hasher=None) # Pop the hashtype byte
|
||||||
|
|
||||||
|
def verifyRawTransaction(self, tx_hex: str, prevouts):
|
||||||
|
# Only checks signature
|
||||||
|
# verifyrawtransaction
|
||||||
|
self._log.warning('NAV verifyRawTransaction only checks signature')
|
||||||
|
inputs_valid: bool = False
|
||||||
|
validscripts: int = 0
|
||||||
|
|
||||||
|
tx_bytes = bytes.fromhex(tx_hex)
|
||||||
|
tx = self.loadTx(bytes.fromhex(tx_hex))
|
||||||
|
|
||||||
|
signature = tx.wit.vtxinwit[0].scriptWitness.stack[0]
|
||||||
|
pubkey = tx.wit.vtxinwit[0].scriptWitness.stack[1]
|
||||||
|
|
||||||
|
input_n: int = 0
|
||||||
|
prevout_data = prevouts[input_n]
|
||||||
|
redeem_script = bytes.fromhex(prevout_data['redeemScript'])
|
||||||
|
prevout_value = self.make_int(prevout_data['amount'])
|
||||||
|
|
||||||
|
if self.verifyTxSig(tx_bytes, signature, pubkey, input_n, redeem_script, prevout_value):
|
||||||
|
validscripts += 1
|
||||||
|
|
||||||
|
# TODO: validate inputs
|
||||||
|
inputs_valid = True
|
||||||
|
|
||||||
|
return {
|
||||||
|
'inputs_valid': inputs_valid,
|
||||||
|
'validscripts': validscripts,
|
||||||
|
}
|
||||||
|
|
||||||
|
def getHTLCSpendTxVSize(self, redeem: bool = True) -> int:
|
||||||
|
tx_vsize = 5 # Add a few bytes, sequence in script takes variable amount of bytes
|
||||||
|
|
||||||
|
tx_vsize += 184 if redeem else 187
|
||||||
|
return tx_vsize
|
||||||
|
|
||||||
|
def getTxid(self, tx) -> bytes:
|
||||||
|
if isinstance(tx, str):
|
||||||
|
tx = bytes.fromhex(tx)
|
||||||
|
if isinstance(tx, bytes):
|
||||||
|
tx = self.loadTx(tx)
|
||||||
|
tx.rehash()
|
||||||
|
return i2b(tx.sha256)
|
||||||
|
|
||||||
|
def rescanBlockchainForAddress(self, height_start: int, addr_find: str):
|
||||||
|
# Very ugly workaround for missing `rescanblockchain` rpc command
|
||||||
|
|
||||||
|
chain_blocks: int = self.getChainHeight()
|
||||||
|
|
||||||
|
current_height: int = chain_blocks
|
||||||
|
block_hash = self.rpc('getblockhash', [current_height])
|
||||||
|
|
||||||
|
script_hash: bytes = self.decodeAddress(addr_find)
|
||||||
|
find_scriptPubKey = self.getDestForScriptHash(script_hash)
|
||||||
|
|
||||||
|
while current_height > height_start:
|
||||||
|
block_hash = self.rpc('getblockhash', [current_height])
|
||||||
|
|
||||||
|
block = self.rpc('getblock', [block_hash, False])
|
||||||
|
decoded_block = CBlock()
|
||||||
|
decoded_block = FromHex(decoded_block, block)
|
||||||
|
for tx in decoded_block.vtx:
|
||||||
|
for txo in tx.vout:
|
||||||
|
if txo.scriptPubKey == find_scriptPubKey:
|
||||||
|
tx.rehash()
|
||||||
|
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('rescanblockchain hack invalidateblock {}'.format(block_hash))
|
||||||
|
self.rpc('invalidateblock', [block_hash])
|
||||||
|
self.rpc('reconsiderblock', [block_hash])
|
||||||
|
return
|
||||||
|
current_height -= 1
|
||||||
|
|
||||||
|
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False):
|
||||||
|
# Add watchonly address and rescan if required
|
||||||
|
|
||||||
|
if not self.isAddressMine(dest_address, or_watch_only=True):
|
||||||
|
self.importWatchOnlyAddress(dest_address, 'bid')
|
||||||
|
self._log.info('Imported watch-only addr: {}'.format(dest_address))
|
||||||
|
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), rescan_from))
|
||||||
|
self.rescanBlockchainForAddress(rescan_from, dest_address)
|
||||||
|
|
||||||
|
return_txid = True if txid is None else False
|
||||||
|
if txid is None:
|
||||||
|
txns = self.rpc('listunspent', [0, 9999999, [dest_address, ]])
|
||||||
|
|
||||||
|
for tx in txns:
|
||||||
|
if self.make_int(tx['amount']) == bid_amount:
|
||||||
|
txid = bytes.fromhex(tx['txid'])
|
||||||
|
break
|
||||||
|
|
||||||
|
if txid is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
tx = self.rpc('gettransaction', [txid.hex()])
|
||||||
|
|
||||||
|
block_height = 0
|
||||||
|
if 'blockhash' in tx:
|
||||||
|
block_header = self.rpc('getblockheader', [tx['blockhash']])
|
||||||
|
block_height = block_header['height']
|
||||||
|
|
||||||
|
rv = {
|
||||||
|
'depth': 0 if 'confirmations' not in tx else tx['confirmations'],
|
||||||
|
'height': block_height}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._log.debug('getLockTxHeight gettransaction failed: %s, %s', txid.hex(), str(e))
|
||||||
|
return None
|
||||||
|
|
||||||
|
if find_index:
|
||||||
|
tx_obj = self.rpc('decoderawtransaction', [tx['hex']])
|
||||||
|
rv['index'] = find_vout_for_address_from_txobj(tx_obj, dest_address)
|
||||||
|
|
||||||
|
if return_txid:
|
||||||
|
rv['txid'] = txid.hex()
|
||||||
|
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def getBlockWithTxns(self, block_hash):
|
||||||
|
# TODO: Bypass decoderawtransaction and getblockheader
|
||||||
|
block = self.rpc('getblock', [block_hash, False])
|
||||||
|
block_header = self.rpc('getblockheader', [block_hash])
|
||||||
|
decoded_block = CBlock()
|
||||||
|
decoded_block = FromHex(decoded_block, block)
|
||||||
|
|
||||||
|
tx_rv = []
|
||||||
|
for tx in decoded_block.vtx:
|
||||||
|
tx_hex = tx.serialize_with_witness().hex()
|
||||||
|
tx_dec = self.rpc('decoderawtransaction', [tx_hex])
|
||||||
|
if 'hex' not in tx_dec:
|
||||||
|
tx_dec['hex'] = tx_hex
|
||||||
|
|
||||||
|
tx_rv.append(tx_dec)
|
||||||
|
|
||||||
|
block_rv = {
|
||||||
|
'hash': block_hash,
|
||||||
|
'tx': tx_rv,
|
||||||
|
'confirmations': block_header['confirmations'],
|
||||||
|
'height': block_header['height'],
|
||||||
|
'version': block_header['version'],
|
||||||
|
'merkleroot': block_header['merkleroot'],
|
||||||
|
}
|
||||||
|
|
||||||
|
return block_rv
|
||||||
|
|
||||||
|
def getScriptScriptSig(self, script: bytes) -> bytearray:
|
||||||
|
return self.getP2SHP2WSHScriptSig(script)
|
||||||
|
|
||||||
|
def getScriptDest(self, script):
|
||||||
|
return self.getP2SHP2WSHDest(script)
|
||||||
|
|
||||||
|
def getDestForScriptHash(self, script_hash):
|
||||||
|
assert len(script_hash) == 20
|
||||||
|
return CScript([OP_HASH160, script_hash, OP_EQUAL])
|
||||||
|
|
||||||
|
def pubkey_to_segwit_address(self, pk: bytes) -> str:
|
||||||
|
pkh = hash160(pk)
|
||||||
|
script_out = self.getScriptForPubkeyHash(pkh)
|
||||||
|
return self.encodeSegwitAddressScript(script_out)
|
||||||
|
|
||||||
|
def createBLockTx(self, Kbs: bytes, output_amount: int, vkbv=None) -> bytes:
|
||||||
|
tx = CTransaction()
|
||||||
|
tx.nVersion = self.txVersion()
|
||||||
|
script_pk = self.getPkDest(Kbs)
|
||||||
|
tx.vout.append(self.txoType()(output_amount, script_pk))
|
||||||
|
return tx.serialize()
|
||||||
|
|
||||||
|
def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee: int, restore_height: int) -> bytes:
|
||||||
|
self._log.info('spendBLockTx %s:\n', chain_b_lock_txid.hex())
|
||||||
|
wtx = self.rpc('gettransaction', [chain_b_lock_txid.hex(), ])
|
||||||
|
lock_tx = self.loadTx(bytes.fromhex(wtx['hex']))
|
||||||
|
|
||||||
|
Kbs = self.getPubkey(kbs)
|
||||||
|
script_pk = self.getPkDest(Kbs)
|
||||||
|
locked_n = findOutput(lock_tx, script_pk)
|
||||||
|
ensure(locked_n is not None, 'Output not found in tx')
|
||||||
|
pkh_to = self.decodeAddress(address_to)
|
||||||
|
|
||||||
|
tx = CTransaction()
|
||||||
|
tx.nVersion = self.txVersion()
|
||||||
|
|
||||||
|
chain_b_lock_txid_int = uint256_from_str(chain_b_lock_txid[::-1])
|
||||||
|
|
||||||
|
script_sig = self.getInputScriptForPubkeyHash(self.getPubkeyHash(Kbs))
|
||||||
|
|
||||||
|
tx.vin.append(CTxIn(COutPoint(chain_b_lock_txid_int, locked_n),
|
||||||
|
nSequence=0,
|
||||||
|
scriptSig=script_sig))
|
||||||
|
tx.vout.append(self.txoType()(cb_swap_value, self.getScriptForPubkeyHash(pkh_to)))
|
||||||
|
|
||||||
|
pay_fee = self.getBLockSpendTxFee(tx, b_fee)
|
||||||
|
tx.vout[0].nValue = cb_swap_value - pay_fee
|
||||||
|
|
||||||
|
b_lock_spend_tx = tx.serialize()
|
||||||
|
b_lock_spend_tx = self.signTxWithKey(b_lock_spend_tx, kbs, cb_swap_value)
|
||||||
|
|
||||||
|
return bytes.fromhex(self.publishTx(b_lock_spend_tx))
|
||||||
|
|
||||||
|
def signTxWithKey(self, tx: bytes, key: bytes, prev_amount: int) -> bytes:
|
||||||
|
Key = self.getPubkey(key)
|
||||||
|
pkh = self.getPubkeyHash(Key)
|
||||||
|
script = self.getScriptForP2PKH(pkh)
|
||||||
|
|
||||||
|
sig = self.signTx(key, tx, 0, script, prev_amount)
|
||||||
|
|
||||||
|
stack = [
|
||||||
|
sig,
|
||||||
|
Key,
|
||||||
|
]
|
||||||
|
return self.setTxSignature(tx, stack)
|
||||||
|
|
||||||
|
def findTxnByHash(self, txid_hex: str):
|
||||||
|
# Only works for wallet txns
|
||||||
|
try:
|
||||||
|
rv = self.rpc('gettransaction', [txid_hex])
|
||||||
|
except Exception as ex:
|
||||||
|
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
|
||||||
|
return None
|
||||||
|
if 'confirmations' in rv and rv['confirmations'] >= self.blocks_confirmed:
|
||||||
|
block_height = self.getBlockHeader(rv['blockhash'])['height']
|
||||||
|
return {'txid': txid_hex, 'amount': 0, 'height': block_height}
|
||||||
|
return None
|
||||||
|
|
||||||
|
def createSCLockTx(self, value: int, script: bytearray, vkbv: bytes = None) -> bytes:
|
||||||
|
tx = CTransaction()
|
||||||
|
tx.nVersion = self.txVersion()
|
||||||
|
tx.vout.append(self.txoType()(value, self.getScriptDest(script)))
|
||||||
|
|
||||||
|
return tx.serialize()
|
||||||
|
|
||||||
|
def fundTx(self, tx_hex: str, feerate: int, lock_unspents: bool = True):
|
||||||
|
feerate_str = self.format_amount(feerate)
|
||||||
|
# TODO: unlock unspents if bid cancelled
|
||||||
|
options = {
|
||||||
|
'lockUnspents': lock_unspents,
|
||||||
|
'feeRate': feerate_str,
|
||||||
|
}
|
||||||
|
rv = self.rpc('fundrawtransaction', [tx_hex, options])
|
||||||
|
|
||||||
|
# Sign transaction then strip witness data to fill scriptsig
|
||||||
|
rv = self.rpc('signrawtransaction', [rv['hex']])
|
||||||
|
|
||||||
|
tx_signed = self.loadTx(bytes.fromhex(rv['hex']))
|
||||||
|
if len(tx_signed.vin) != len(tx_signed.wit.vtxinwit):
|
||||||
|
raise ValueError('txn has non segwit input')
|
||||||
|
for witness_data in tx_signed.wit.vtxinwit:
|
||||||
|
if len(witness_data.scriptWitness.stack) < 2:
|
||||||
|
raise ValueError('txn has non segwit input')
|
||||||
|
|
||||||
|
return tx_signed.serialize_without_witness()
|
||||||
|
|
||||||
|
def fundSCLockTx(self, tx_bytes: bytes, feerate, vkbv=None) -> bytes:
|
||||||
|
tx_funded = self.fundTx(tx_bytes.hex(), feerate)
|
||||||
|
return tx_funded
|
||||||
|
|
||||||
|
def createSCLockRefundTx(self, tx_lock_bytes, script_lock, Kal, Kaf, lock1_value, csv_val, tx_fee_rate, vkbv=None):
|
||||||
|
tx_lock = CTransaction()
|
||||||
|
tx_lock = self.loadTx(tx_lock_bytes)
|
||||||
|
|
||||||
|
output_script = self.getScriptDest(script_lock)
|
||||||
|
locked_n = findOutput(tx_lock, output_script)
|
||||||
|
ensure(locked_n is not None, 'Output not found in tx')
|
||||||
|
locked_coin = tx_lock.vout[locked_n].nValue
|
||||||
|
|
||||||
|
tx_lock.rehash()
|
||||||
|
tx_lock_id_int = tx_lock.sha256
|
||||||
|
|
||||||
|
refund_script = self.genScriptLockRefundTxScript(Kal, Kaf, csv_val)
|
||||||
|
tx = CTransaction()
|
||||||
|
tx.nVersion = self.txVersion()
|
||||||
|
tx.vin.append(CTxIn(COutPoint(tx_lock_id_int, locked_n),
|
||||||
|
nSequence=lock1_value,
|
||||||
|
scriptSig=self.getScriptScriptSig(script_lock)))
|
||||||
|
tx.vout.append(self.txoType()(locked_coin, self.getScriptDest(refund_script)))
|
||||||
|
|
||||||
|
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
|
||||||
|
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
|
||||||
|
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
|
||||||
|
pay_fee = round(tx_fee_rate * vsize / 1000)
|
||||||
|
tx.vout[0].nValue = locked_coin - pay_fee
|
||||||
|
|
||||||
|
tx.rehash()
|
||||||
|
self._log.info('createSCLockRefundTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.',
|
||||||
|
i2h(tx.sha256), tx_fee_rate, vsize, pay_fee)
|
||||||
|
|
||||||
|
return tx.serialize(), refund_script, tx.vout[0].nValue
|
||||||
|
|
||||||
|
def createSCLockRefundSpendTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_refund_to, tx_fee_rate, vkbv=None):
|
||||||
|
# Returns the coinA locked coin to the leader
|
||||||
|
# The follower will sign the multisig path with a signature encumbered by the leader's coinB spend pubkey
|
||||||
|
# If the leader publishes the decrypted signature the leader's coinB spend privatekey will be revealed to the follower
|
||||||
|
|
||||||
|
tx_lock_refund = self.loadTx(tx_lock_refund_bytes)
|
||||||
|
|
||||||
|
output_script = self.getScriptDest(script_lock_refund)
|
||||||
|
locked_n = findOutput(tx_lock_refund, output_script)
|
||||||
|
ensure(locked_n is not None, 'Output not found in tx')
|
||||||
|
locked_coin = tx_lock_refund.vout[locked_n].nValue
|
||||||
|
|
||||||
|
tx_lock_refund.rehash()
|
||||||
|
tx_lock_refund_hash_int = tx_lock_refund.sha256
|
||||||
|
|
||||||
|
tx = CTransaction()
|
||||||
|
tx.nVersion = self.txVersion()
|
||||||
|
tx.vin.append(CTxIn(COutPoint(tx_lock_refund_hash_int, locked_n),
|
||||||
|
nSequence=0,
|
||||||
|
scriptSig=self.getScriptScriptSig(script_lock_refund)))
|
||||||
|
|
||||||
|
tx.vout.append(self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_refund_to)))
|
||||||
|
|
||||||
|
dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(script_lock_refund)
|
||||||
|
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
|
||||||
|
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
|
||||||
|
pay_fee = round(tx_fee_rate * vsize / 1000)
|
||||||
|
tx.vout[0].nValue = locked_coin - pay_fee
|
||||||
|
|
||||||
|
tx.rehash()
|
||||||
|
self._log.info('createSCLockRefundSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.',
|
||||||
|
i2h(tx.sha256), tx_fee_rate, vsize, pay_fee)
|
||||||
|
|
||||||
|
return tx.serialize()
|
||||||
|
|
||||||
|
def createSCLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv=None):
|
||||||
|
# lock refund swipe tx
|
||||||
|
# Sends the coinA locked coin to the follower
|
||||||
|
|
||||||
|
tx_lock_refund = self.loadTx(tx_lock_refund_bytes)
|
||||||
|
|
||||||
|
output_script = self.getScriptDest(script_lock_refund)
|
||||||
|
locked_n = findOutput(tx_lock_refund, output_script)
|
||||||
|
ensure(locked_n is not None, 'Output not found in tx')
|
||||||
|
locked_coin = tx_lock_refund.vout[locked_n].nValue
|
||||||
|
|
||||||
|
A, B, lock2_value, C = self.extractScriptLockRefundScriptValues(script_lock_refund)
|
||||||
|
|
||||||
|
tx_lock_refund.rehash()
|
||||||
|
tx_lock_refund_hash_int = tx_lock_refund.sha256
|
||||||
|
|
||||||
|
tx = CTransaction()
|
||||||
|
tx.nVersion = self.txVersion()
|
||||||
|
tx.vin.append(CTxIn(COutPoint(tx_lock_refund_hash_int, locked_n),
|
||||||
|
nSequence=lock2_value,
|
||||||
|
scriptSig=self.getScriptScriptSig(script_lock_refund)))
|
||||||
|
|
||||||
|
tx.vout.append(self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_dest)))
|
||||||
|
|
||||||
|
dummy_witness_stack = self.getScriptLockRefundSwipeTxDummyWitness(script_lock_refund)
|
||||||
|
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
|
||||||
|
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
|
||||||
|
pay_fee = round(tx_fee_rate * vsize / 1000)
|
||||||
|
tx.vout[0].nValue = locked_coin - pay_fee
|
||||||
|
|
||||||
|
tx.rehash()
|
||||||
|
self._log.info('createSCLockRefundSpendToFTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.',
|
||||||
|
i2h(tx.sha256), tx_fee_rate, vsize, pay_fee)
|
||||||
|
|
||||||
|
return tx.serialize()
|
||||||
|
|
||||||
|
def createSCLockSpendTx(self, tx_lock_bytes, script_lock, pkh_dest, tx_fee_rate, vkbv=None, fee_info={}):
|
||||||
|
tx_lock = self.loadTx(tx_lock_bytes)
|
||||||
|
output_script = self.getScriptDest(script_lock)
|
||||||
|
locked_n = findOutput(tx_lock, output_script)
|
||||||
|
ensure(locked_n is not None, 'Output not found in tx')
|
||||||
|
locked_coin = tx_lock.vout[locked_n].nValue
|
||||||
|
|
||||||
|
tx_lock.rehash()
|
||||||
|
tx_lock_id_int = tx_lock.sha256
|
||||||
|
|
||||||
|
tx = CTransaction()
|
||||||
|
tx.nVersion = self.txVersion()
|
||||||
|
tx.vin.append(CTxIn(COutPoint(tx_lock_id_int, locked_n),
|
||||||
|
scriptSig=self.getScriptScriptSig(script_lock)))
|
||||||
|
|
||||||
|
tx.vout.append(self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_dest)))
|
||||||
|
|
||||||
|
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
|
||||||
|
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
|
||||||
|
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
|
||||||
|
pay_fee = round(tx_fee_rate * vsize / 1000)
|
||||||
|
tx.vout[0].nValue = locked_coin - pay_fee
|
||||||
|
|
||||||
|
fee_info['fee_paid'] = pay_fee
|
||||||
|
fee_info['rate_used'] = tx_fee_rate
|
||||||
|
fee_info['witness_bytes'] = witness_bytes
|
||||||
|
fee_info['vsize'] = vsize
|
||||||
|
|
||||||
|
tx.rehash()
|
||||||
|
self._log.info('createSCLockSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.',
|
||||||
|
i2h(tx.sha256), tx_fee_rate, vsize, pay_fee)
|
||||||
|
|
||||||
|
return tx.serialize()
|
||||||
@@ -19,7 +19,7 @@ class NMCInterface(BTCInterface):
|
|||||||
|
|
||||||
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index=False):
|
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index=False):
|
||||||
self._log.debug('[rm] scantxoutset start') # scantxoutset is slow
|
self._log.debug('[rm] scantxoutset start') # scantxoutset is slow
|
||||||
ro = self.rpc_callback('scantxoutset', ['start', ['addr({})'.format(dest_address)]]) # TODO: Use combo(address) where possible
|
ro = self.rpc('scantxoutset', ['start', ['addr({})'.format(dest_address)]]) # TODO: Use combo(address) where possible
|
||||||
self._log.debug('[rm] scantxoutset end')
|
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']:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2020-2022 tecnovert
|
# Copyright (c) 2020-2024 tecnovert
|
||||||
# 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,7 +17,6 @@ from basicswap.contrib.test_framework.script import (
|
|||||||
OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG
|
OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG
|
||||||
)
|
)
|
||||||
from basicswap.util import (
|
from basicswap.util import (
|
||||||
i2b,
|
|
||||||
ensure,
|
ensure,
|
||||||
make_int,
|
make_int,
|
||||||
TemporaryError,
|
TemporaryError,
|
||||||
@@ -43,6 +42,8 @@ class BalanceTypes(IntEnum):
|
|||||||
class PARTInterface(BTCInterface):
|
class PARTInterface(BTCInterface):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def coin_type():
|
def coin_type():
|
||||||
|
# Returns the base coin type
|
||||||
|
# ANON and BLIND PART will return Coins.PART
|
||||||
return Coins.PART
|
return Coins.PART
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -58,8 +59,12 @@ class PARTInterface(BTCInterface):
|
|||||||
return 0xa0
|
return 0xa0
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def xmr_swap_alock_spend_tx_vsize() -> int:
|
def xmr_swap_a_lock_spend_tx_vsize() -> int:
|
||||||
return 213
|
return 200
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def xmr_swap_b_lock_spend_tx_vsize() -> int:
|
||||||
|
return 138
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def txoType():
|
def txoType():
|
||||||
@@ -77,15 +82,15 @@ class PARTInterface(BTCInterface):
|
|||||||
# TODO: Double check
|
# TODO: Double check
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def getNewAddress(self, use_segwit, label='swap_receive'):
|
def getNewAddress(self, use_segwit, label='swap_receive') -> str:
|
||||||
return self.rpc_callback('getnewaddress', [label])
|
return self.rpc_wallet('getnewaddress', [label])
|
||||||
|
|
||||||
def getNewStealthAddress(self, label='swap_stealth'):
|
def getNewStealthAddress(self, label='swap_stealth') -> str:
|
||||||
return self.rpc_callback('getnewstealthaddress', [label])
|
return self.rpc_wallet('getnewstealthaddress', [label])
|
||||||
|
|
||||||
def haveSpentIndex(self):
|
def haveSpentIndex(self):
|
||||||
version = self.getDaemonVersion()
|
version = self.getDaemonVersion()
|
||||||
index_info = self.rpc_callback('getinsightinfo' if int(str(version)[:2]) > 19 else 'getindexinfo')
|
index_info = self.rpc('getinsightinfo' if int(str(version)[:2]) > 19 else 'getindexinfo')
|
||||||
return index_info['spentindex']
|
return index_info['spentindex']
|
||||||
|
|
||||||
def initialiseWallet(self, key):
|
def initialiseWallet(self, key):
|
||||||
@@ -93,33 +98,33 @@ class PARTInterface(BTCInterface):
|
|||||||
|
|
||||||
def withdrawCoin(self, value, addr_to, subfee):
|
def withdrawCoin(self, value, addr_to, subfee):
|
||||||
params = [addr_to, value, '', '', subfee, '', True, self._conf_target]
|
params = [addr_to, value, '', '', subfee, '', True, self._conf_target]
|
||||||
return self.rpc_callback('sendtoaddress', params)
|
return self.rpc_wallet('sendtoaddress', params)
|
||||||
|
|
||||||
def sendTypeTo(self, type_from, type_to, value, addr_to, subfee):
|
def sendTypeTo(self, type_from, type_to, value, addr_to, subfee):
|
||||||
params = [type_from, type_to,
|
params = [type_from, type_to,
|
||||||
[{'address': addr_to, 'amount': value, 'subfee': subfee}, ],
|
[{'address': addr_to, 'amount': value, 'subfee': subfee}, ],
|
||||||
'', '', self._anon_tx_ring_size, 1, False,
|
'', '', self._anon_tx_ring_size, 1, False,
|
||||||
{'conf_target': self._conf_target}]
|
{'conf_target': self._conf_target}]
|
||||||
return self.rpc_callback('sendtypeto', params)
|
return self.rpc_wallet('sendtypeto', params)
|
||||||
|
|
||||||
def getScriptForPubkeyHash(self, pkh):
|
def getScriptForPubkeyHash(self, pkh: bytes) -> CScript:
|
||||||
return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG])
|
return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG])
|
||||||
|
|
||||||
def formatStealthAddress(self, scan_pubkey, spend_pubkey):
|
def formatStealthAddress(self, scan_pubkey, spend_pubkey) -> str:
|
||||||
prefix_byte = chainparams[self.coin_type()][self._network]['stealth_key_prefix']
|
prefix_byte = chainparams[self.coin_type()][self._network]['stealth_key_prefix']
|
||||||
|
|
||||||
return encodeStealthAddress(prefix_byte, scan_pubkey, spend_pubkey)
|
return encodeStealthAddress(prefix_byte, scan_pubkey, spend_pubkey)
|
||||||
|
|
||||||
def getWitnessStackSerialisedLength(self, witness_stack):
|
def getWitnessStackSerialisedLength(self, witness_stack) -> int:
|
||||||
length = getCompactSizeLen(len(witness_stack))
|
length: int = getCompactSizeLen(len(witness_stack))
|
||||||
for e in witness_stack:
|
for e in witness_stack:
|
||||||
length += getWitnessElementLen(len(e) // 2) # hex -> bytes
|
length += getWitnessElementLen(len(e))
|
||||||
return length
|
return length
|
||||||
|
|
||||||
def getWalletRestoreHeight(self):
|
def getWalletRestoreHeight(self) -> int:
|
||||||
start_time = self.rpc_callback('getwalletinfo')['keypoololdest']
|
start_time = self.rpc_wallet('getwalletinfo')['keypoololdest']
|
||||||
|
|
||||||
blockchaininfo = self.rpc_callback('getblockchaininfo')
|
blockchaininfo = self.getBlockchainInfo()
|
||||||
best_block = blockchaininfo['bestblockhash']
|
best_block = blockchaininfo['bestblockhash']
|
||||||
|
|
||||||
chain_synced = round(blockchaininfo['verificationprogress'], 3)
|
chain_synced = round(blockchaininfo['verificationprogress'], 3)
|
||||||
@@ -127,17 +132,41 @@ class PARTInterface(BTCInterface):
|
|||||||
raise ValueError('{} chain isn\'t synced.'.format(self.coin_name()))
|
raise ValueError('{} chain isn\'t synced.'.format(self.coin_name()))
|
||||||
|
|
||||||
self._log.debug('Finding block at time: {}'.format(start_time))
|
self._log.debug('Finding block at time: {}'.format(start_time))
|
||||||
block_hash = self.rpc_callback('getblockhashafter', [start_time])
|
block_hash = self.rpc('getblockhashafter', [start_time])
|
||||||
block_header = self.rpc_callback('getblockheader', [block_hash])
|
block_header = self.rpc('getblockheader', [block_hash])
|
||||||
return block_header['height']
|
return block_header['height']
|
||||||
|
|
||||||
|
def getHTLCSpendTxVSize(self, redeem: bool = True) -> int:
|
||||||
|
tx_vsize = 5 # Add a few bytes, sequence in script takes variable amount of bytes
|
||||||
|
tx_vsize += 204 if redeem else 187
|
||||||
|
return tx_vsize
|
||||||
|
|
||||||
|
def getUnspentsByAddr(self):
|
||||||
|
unspent_addr = dict()
|
||||||
|
unspent = self.rpc_wallet('listunspent')
|
||||||
|
for u in unspent:
|
||||||
|
if u['spendable'] is not True:
|
||||||
|
continue
|
||||||
|
if 'address' not in u:
|
||||||
|
continue
|
||||||
|
unspent_addr[u['address']] = unspent_addr.get(u['address'], 0) + self.make_int(u['amount'], r=1)
|
||||||
|
return unspent_addr
|
||||||
|
|
||||||
|
|
||||||
class PARTInterfaceBlind(PARTInterface):
|
class PARTInterfaceBlind(PARTInterface):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def balance_type():
|
def balance_type():
|
||||||
return BalanceTypes.BLIND
|
return BalanceTypes.BLIND
|
||||||
|
|
||||||
def coin_name(self):
|
@staticmethod
|
||||||
|
def xmr_swap_a_lock_spend_tx_vsize() -> int:
|
||||||
|
return 1032
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def xmr_swap_b_lock_spend_tx_vsize() -> int:
|
||||||
|
return 980
|
||||||
|
|
||||||
|
def coin_name(self) -> str:
|
||||||
return super().coin_name() + ' Blind'
|
return super().coin_name() + ' Blind'
|
||||||
|
|
||||||
def getScriptLockTxNonce(self, data):
|
def getScriptLockTxNonce(self, data):
|
||||||
@@ -153,24 +182,23 @@ class PARTInterfaceBlind(PARTInterface):
|
|||||||
if txo['type'] != 'blind':
|
if txo['type'] != 'blind':
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
blinded_info = self.rpc_callback('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], nonce.hex()])
|
blinded_info = self.rpc('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], nonce.hex()])
|
||||||
output_n = txo['n']
|
output_n = txo['n']
|
||||||
|
|
||||||
self.rpc_callback('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], nonce.hex()])
|
self.rpc('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], nonce.hex()])
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._log.debug('Searching for locked output: {}'.format(str(e)))
|
self._log.debug('Searching for locked output: {}'.format(str(e)))
|
||||||
continue
|
continue
|
||||||
# Should not be possible for commitment not to match
|
# Should not be possible for commitment not to match
|
||||||
v = self.rpc_callback('verifycommitment', [txo['valueCommitment'], blinded_info['blind'], blinded_info['amount']])
|
v = self.rpc('verifycommitment', [txo['valueCommitment'], blinded_info['blind'], blinded_info['amount']])
|
||||||
ensure(v['result'] is True, 'verifycommitment failed')
|
ensure(v['result'] is True, 'verifycommitment failed')
|
||||||
return output_n, blinded_info
|
return output_n, blinded_info
|
||||||
|
|
||||||
def createScriptLockTx(self, value, Kal, Kaf, vkbv):
|
def createSCLockTx(self, value: int, script: bytearray, vkbv: bytes) -> bytes:
|
||||||
script = self.genScriptLockTxScript(Kal, Kaf)
|
|
||||||
|
|
||||||
# Nonce is derived from vkbv, ephemeral_key isn't used
|
# Nonce is derived from vkbv, ephemeral_key isn't used
|
||||||
ephemeral_key = i2b(self.getNewSecretKey())
|
ephemeral_key = self.getNewSecretKey()
|
||||||
ephemeral_pubkey = self.getPubkey(ephemeral_key)
|
ephemeral_pubkey = self.getPubkey(ephemeral_key)
|
||||||
assert (len(ephemeral_pubkey) == 33)
|
assert (len(ephemeral_pubkey) == 33)
|
||||||
nonce = self.getScriptLockTxNonce(vkbv)
|
nonce = self.getScriptLockTxNonce(vkbv)
|
||||||
@@ -178,23 +206,23 @@ class PARTInterfaceBlind(PARTInterface):
|
|||||||
inputs = []
|
inputs = []
|
||||||
outputs = [{'type': 'blind', 'amount': self.format_amount(value), 'address': p2wsh_addr, 'nonce': nonce.hex(), 'data': ephemeral_pubkey.hex()}]
|
outputs = [{'type': 'blind', 'amount': self.format_amount(value), 'address': p2wsh_addr, 'nonce': nonce.hex(), 'data': ephemeral_pubkey.hex()}]
|
||||||
params = [inputs, outputs]
|
params = [inputs, outputs]
|
||||||
rv = self.rpc_callback('createrawparttransaction', params)
|
rv = self.rpc_wallet('createrawparttransaction', params)
|
||||||
|
|
||||||
tx_bytes = bytes.fromhex(rv['hex'])
|
tx_bytes = bytes.fromhex(rv['hex'])
|
||||||
return tx_bytes, script
|
return tx_bytes
|
||||||
|
|
||||||
def fundScriptLockTx(self, tx_bytes, feerate, vkbv):
|
def fundSCLockTx(self, tx_bytes: bytes, feerate: int, vkbv: bytes) -> bytes:
|
||||||
feerate_str = self.format_amount(feerate)
|
feerate_str = self.format_amount(feerate)
|
||||||
# TODO: unlock unspents if bid cancelled
|
# TODO: unlock unspents if bid cancelled
|
||||||
|
|
||||||
tx_hex = tx_bytes.hex()
|
tx_hex = tx_bytes.hex()
|
||||||
nonce = self.getScriptLockTxNonce(vkbv)
|
nonce = self.getScriptLockTxNonce(vkbv)
|
||||||
|
|
||||||
tx_obj = self.rpc_callback('decoderawtransaction', [tx_hex])
|
tx_obj = self.rpc('decoderawtransaction', [tx_hex])
|
||||||
|
|
||||||
assert (len(tx_obj['vout']) == 1)
|
assert (len(tx_obj['vout']) == 1)
|
||||||
txo = tx_obj['vout'][0]
|
txo = tx_obj['vout'][0]
|
||||||
blinded_info = self.rpc_callback('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], nonce.hex()])
|
blinded_info = self.rpc('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], nonce.hex()])
|
||||||
|
|
||||||
outputs_info = {0: {'value': blinded_info['amount'], 'blind': blinded_info['blind'], 'nonce': nonce.hex()}}
|
outputs_info = {0: {'value': blinded_info['amount'], 'blind': blinded_info['blind'], 'nonce': nonce.hex()}}
|
||||||
|
|
||||||
@@ -202,14 +230,14 @@ class PARTInterfaceBlind(PARTInterface):
|
|||||||
'lockUnspents': True,
|
'lockUnspents': True,
|
||||||
'feeRate': feerate_str,
|
'feeRate': feerate_str,
|
||||||
}
|
}
|
||||||
rv = self.rpc_callback('fundrawtransactionfrom', ['blind', tx_hex, {}, outputs_info, options])
|
rv = self.rpc('fundrawtransactionfrom', ['blind', tx_hex, {}, outputs_info, options])
|
||||||
return bytes.fromhex(rv['hex'])
|
return bytes.fromhex(rv['hex'])
|
||||||
|
|
||||||
def createScriptLockRefundTx(self, tx_lock_bytes, script_lock, Kal, Kaf, lock1_value, csv_val, tx_fee_rate, vkbv):
|
def createSCLockRefundTx(self, tx_lock_bytes, script_lock, Kal, Kaf, lock1_value, csv_val, tx_fee_rate, vkbv):
|
||||||
lock_tx_obj = self.rpc_callback('decoderawtransaction', [tx_lock_bytes.hex()])
|
lock_tx_obj = self.rpc('decoderawtransaction', [tx_lock_bytes.hex()])
|
||||||
assert (self.getTxid(tx_lock_bytes).hex() == lock_tx_obj['txid'])
|
assert (self.getTxid(tx_lock_bytes).hex() == lock_tx_obj['txid'])
|
||||||
# Nonce is derived from vkbv, ephemeral_key isn't used
|
# Nonce is derived from vkbv, ephemeral_key isn't used
|
||||||
ephemeral_key = i2b(self.getNewSecretKey())
|
ephemeral_key = self.getNewSecretKey()
|
||||||
ephemeral_pubkey = self.getPubkey(ephemeral_key)
|
ephemeral_pubkey = self.getPubkey(ephemeral_key)
|
||||||
assert (len(ephemeral_pubkey) == 33)
|
assert (len(ephemeral_pubkey) == 33)
|
||||||
nonce = self.getScriptLockTxNonce(vkbv)
|
nonce = self.getScriptLockTxNonce(vkbv)
|
||||||
@@ -227,14 +255,15 @@ class PARTInterfaceBlind(PARTInterface):
|
|||||||
inputs = [{'txid': tx_lock_id, 'vout': spend_n, 'sequence': lock1_value, 'blindingfactor': input_blinded_info['blind']}]
|
inputs = [{'txid': tx_lock_id, 'vout': spend_n, 'sequence': lock1_value, 'blindingfactor': input_blinded_info['blind']}]
|
||||||
outputs = [{'type': 'blind', 'amount': locked_coin, 'address': p2wsh_addr, 'nonce': output_nonce.hex(), 'data': ephemeral_pubkey.hex()}]
|
outputs = [{'type': 'blind', 'amount': locked_coin, 'address': p2wsh_addr, 'nonce': output_nonce.hex(), 'data': ephemeral_pubkey.hex()}]
|
||||||
params = [inputs, outputs]
|
params = [inputs, outputs]
|
||||||
rv = self.rpc_callback('createrawparttransaction', params)
|
rv = self.rpc_wallet('createrawparttransaction', params)
|
||||||
lock_refund_tx_hex = rv['hex']
|
lock_refund_tx_hex = rv['hex']
|
||||||
|
|
||||||
# Set dummy witness data for fee estimation
|
# Set dummy witness data for fee estimation
|
||||||
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
|
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
|
||||||
|
dummy_witness_stack = [x.hex() for x in dummy_witness_stack]
|
||||||
|
|
||||||
# Use a junk change pubkey to avoid adding unused keys to the wallet
|
# Use a junk change pubkey to avoid adding unused keys to the wallet
|
||||||
zero_change_key = i2b(self.getNewSecretKey())
|
zero_change_key = self.getNewSecretKey()
|
||||||
zero_change_pubkey = self.getPubkey(zero_change_key)
|
zero_change_pubkey = self.getPubkey(zero_change_key)
|
||||||
inputs_info = {'0': {'value': input_blinded_info['amount'], 'blind': input_blinded_info['blind'], 'witnessstack': dummy_witness_stack}}
|
inputs_info = {'0': {'value': input_blinded_info['amount'], 'blind': input_blinded_info['blind'], 'witnessstack': dummy_witness_stack}}
|
||||||
outputs_info = rv['amounts']
|
outputs_info = rv['amounts']
|
||||||
@@ -243,7 +272,7 @@ class PARTInterfaceBlind(PARTInterface):
|
|||||||
'feeRate': self.format_amount(tx_fee_rate),
|
'feeRate': self.format_amount(tx_fee_rate),
|
||||||
'subtractFeeFromOutputs': [0, ]
|
'subtractFeeFromOutputs': [0, ]
|
||||||
}
|
}
|
||||||
rv = self.rpc_callback('fundrawtransactionfrom', ['blind', lock_refund_tx_hex, inputs_info, outputs_info, options])
|
rv = self.rpc_wallet('fundrawtransactionfrom', ['blind', lock_refund_tx_hex, inputs_info, outputs_info, options])
|
||||||
lock_refund_tx_hex = rv['hex']
|
lock_refund_tx_hex = rv['hex']
|
||||||
|
|
||||||
for vout, txo in rv['output_amounts'].items():
|
for vout, txo in rv['output_amounts'].items():
|
||||||
@@ -252,12 +281,12 @@ class PARTInterfaceBlind(PARTInterface):
|
|||||||
|
|
||||||
return bytes.fromhex(lock_refund_tx_hex), refund_script, refunded_value
|
return bytes.fromhex(lock_refund_tx_hex), refund_script, refunded_value
|
||||||
|
|
||||||
def createScriptLockRefundSpendTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_refund_to, tx_fee_rate, vkbv):
|
def createSCLockRefundSpendTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_refund_to, tx_fee_rate, vkbv):
|
||||||
# Returns the coinA locked coin to the leader
|
# Returns the coinA locked coin to the leader
|
||||||
# The follower will sign the multisig path with a signature encumbered by the leader's coinB spend pubkey
|
# The follower will sign the multisig path with a signature encumbered by the leader's coinB spend pubkey
|
||||||
# If the leader publishes the decrypted signature the leader's coinB spend privatekey will be revealed to the follower
|
# If the leader publishes the decrypted signature the leader's coinB spend privatekey will be revealed to the follower
|
||||||
|
|
||||||
lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [tx_lock_refund_bytes.hex()])
|
lock_refund_tx_obj = self.rpc('decoderawtransaction', [tx_lock_refund_bytes.hex()])
|
||||||
# Nonce is derived from vkbv
|
# Nonce is derived from vkbv
|
||||||
nonce = self.getScriptLockRefundTxNonce(vkbv)
|
nonce = self.getScriptLockRefundTxNonce(vkbv)
|
||||||
|
|
||||||
@@ -267,7 +296,7 @@ class PARTInterfaceBlind(PARTInterface):
|
|||||||
|
|
||||||
tx_lock_refund_id = lock_refund_tx_obj['txid']
|
tx_lock_refund_id = lock_refund_tx_obj['txid']
|
||||||
addr_out = self.pkh_to_address(pkh_refund_to)
|
addr_out = self.pkh_to_address(pkh_refund_to)
|
||||||
addr_info = self.rpc_callback('getaddressinfo', [addr_out])
|
addr_info = self.rpc_wallet('getaddressinfo', [addr_out])
|
||||||
output_pubkey_hex = addr_info['pubkey']
|
output_pubkey_hex = addr_info['pubkey']
|
||||||
|
|
||||||
# Follower won't be able to decode output to check amount, shouldn't matter as fee is public and output is to leader, sum has to balance
|
# Follower won't be able to decode output to check amount, shouldn't matter as fee is public and output is to leader, sum has to balance
|
||||||
@@ -275,14 +304,15 @@ class PARTInterfaceBlind(PARTInterface):
|
|||||||
inputs = [{'txid': tx_lock_refund_id, 'vout': spend_n, 'sequence': 0, 'blindingfactor': input_blinded_info['blind']}]
|
inputs = [{'txid': tx_lock_refund_id, 'vout': spend_n, 'sequence': 0, 'blindingfactor': input_blinded_info['blind']}]
|
||||||
outputs = [{'type': 'blind', 'amount': input_blinded_info['amount'], 'address': addr_out, 'pubkey': output_pubkey_hex}]
|
outputs = [{'type': 'blind', 'amount': input_blinded_info['amount'], 'address': addr_out, 'pubkey': output_pubkey_hex}]
|
||||||
params = [inputs, outputs]
|
params = [inputs, outputs]
|
||||||
rv = self.rpc_callback('createrawparttransaction', params)
|
rv = self.rpc_wallet('createrawparttransaction', params)
|
||||||
lock_refund_spend_tx_hex = rv['hex']
|
lock_refund_spend_tx_hex = rv['hex']
|
||||||
|
|
||||||
# Set dummy witness data for fee estimation
|
# Set dummy witness data for fee estimation
|
||||||
dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(script_lock_refund)
|
dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(script_lock_refund)
|
||||||
|
dummy_witness_stack = [x.hex() for x in dummy_witness_stack]
|
||||||
|
|
||||||
# Use a junk change pubkey to avoid adding unused keys to the wallet
|
# Use a junk change pubkey to avoid adding unused keys to the wallet
|
||||||
zero_change_key = i2b(self.getNewSecretKey())
|
zero_change_key = self.getNewSecretKey()
|
||||||
zero_change_pubkey = self.getPubkey(zero_change_key)
|
zero_change_pubkey = self.getPubkey(zero_change_key)
|
||||||
inputs_info = {'0': {'value': input_blinded_info['amount'], 'blind': input_blinded_info['blind'], 'witnessstack': dummy_witness_stack}}
|
inputs_info = {'0': {'value': input_blinded_info['amount'], 'blind': input_blinded_info['blind'], 'witnessstack': dummy_witness_stack}}
|
||||||
outputs_info = rv['amounts']
|
outputs_info = rv['amounts']
|
||||||
@@ -292,17 +322,17 @@ class PARTInterfaceBlind(PARTInterface):
|
|||||||
'subtractFeeFromOutputs': [0, ]
|
'subtractFeeFromOutputs': [0, ]
|
||||||
}
|
}
|
||||||
|
|
||||||
rv = self.rpc_callback('fundrawtransactionfrom', ['blind', lock_refund_spend_tx_hex, inputs_info, outputs_info, options])
|
rv = self.rpc_wallet('fundrawtransactionfrom', ['blind', lock_refund_spend_tx_hex, inputs_info, outputs_info, options])
|
||||||
lock_refund_spend_tx_hex = rv['hex']
|
lock_refund_spend_tx_hex = rv['hex']
|
||||||
|
|
||||||
return bytes.fromhex(lock_refund_spend_tx_hex)
|
return bytes.fromhex(lock_refund_spend_tx_hex)
|
||||||
|
|
||||||
def verifyLockTx(self, tx_bytes, script_out,
|
def verifySCLockTx(self, tx_bytes, script_out,
|
||||||
swap_value,
|
swap_value,
|
||||||
Kal, Kaf,
|
Kal, Kaf,
|
||||||
feerate,
|
feerate,
|
||||||
check_lock_tx_inputs, vkbv):
|
check_lock_tx_inputs, vkbv):
|
||||||
lock_tx_obj = self.rpc_callback('decoderawtransaction', [tx_bytes.hex()])
|
lock_tx_obj = self.rpc('decoderawtransaction', [tx_bytes.hex()])
|
||||||
lock_txid_hex = lock_tx_obj['txid']
|
lock_txid_hex = lock_tx_obj['txid']
|
||||||
self._log.info('Verifying lock tx: {}.'.format(lock_txid_hex))
|
self._log.info('Verifying lock tx: {}.'.format(lock_txid_hex))
|
||||||
|
|
||||||
@@ -341,10 +371,10 @@ class PARTInterfaceBlind(PARTInterface):
|
|||||||
|
|
||||||
return bytes.fromhex(lock_txid_hex), lock_output_n
|
return bytes.fromhex(lock_txid_hex), lock_output_n
|
||||||
|
|
||||||
def verifyLockRefundTx(self, tx_bytes, lock_tx_bytes, script_out,
|
def verifySCLockRefundTx(self, tx_bytes, lock_tx_bytes, script_out,
|
||||||
prevout_id, prevout_n, prevout_seq, prevout_script,
|
prevout_id, prevout_n, prevout_seq, prevout_script,
|
||||||
Kal, Kaf, csv_val_expect, swap_value, feerate, vkbv):
|
Kal, Kaf, csv_val_expect, swap_value, feerate, vkbv):
|
||||||
lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [tx_bytes.hex()])
|
lock_refund_tx_obj = self.rpc('decoderawtransaction', [tx_bytes.hex()])
|
||||||
lock_refund_txid_hex = lock_refund_tx_obj['txid']
|
lock_refund_txid_hex = lock_refund_tx_obj['txid']
|
||||||
self._log.info('Verifying lock refund tx: {}.'.format(lock_refund_txid_hex))
|
self._log.info('Verifying lock refund tx: {}.'.format(lock_refund_txid_hex))
|
||||||
|
|
||||||
@@ -377,10 +407,10 @@ class PARTInterfaceBlind(PARTInterface):
|
|||||||
ensure(C == Kaf, 'Bad script pubkey')
|
ensure(C == Kaf, 'Bad script pubkey')
|
||||||
|
|
||||||
# Check rangeproofs and commitments sum
|
# Check rangeproofs and commitments sum
|
||||||
lock_tx_obj = self.rpc_callback('decoderawtransaction', [lock_tx_bytes.hex()])
|
lock_tx_obj = self.rpc('decoderawtransaction', [lock_tx_bytes.hex()])
|
||||||
prevout = lock_tx_obj['vout'][prevout_n]
|
prevout = lock_tx_obj['vout'][prevout_n]
|
||||||
prevtxns = [{'txid': prevout_id.hex(), 'vout': prevout_n, 'scriptPubKey': prevout['scriptPubKey']['hex'], 'amount_commitment': prevout['valueCommitment']}]
|
prevtxns = [{'txid': prevout_id.hex(), 'vout': prevout_n, 'scriptPubKey': prevout['scriptPubKey']['hex'], 'amount_commitment': prevout['valueCommitment']}]
|
||||||
rv = self.rpc_callback('verifyrawtransaction', [tx_bytes.hex(), prevtxns])
|
rv = self.rpc('verifyrawtransaction', [tx_bytes.hex(), prevtxns])
|
||||||
ensure(rv['outputs_valid'] is True, 'Invalid outputs')
|
ensure(rv['outputs_valid'] is True, 'Invalid outputs')
|
||||||
ensure(rv['inputs_valid'] is True, 'Invalid inputs')
|
ensure(rv['inputs_valid'] is True, 'Invalid inputs')
|
||||||
|
|
||||||
@@ -399,11 +429,11 @@ class PARTInterfaceBlind(PARTInterface):
|
|||||||
|
|
||||||
return bytes.fromhex(lock_refund_txid_hex), lock_refund_txo_value, lock_refund_output_n
|
return bytes.fromhex(lock_refund_txid_hex), lock_refund_txo_value, lock_refund_output_n
|
||||||
|
|
||||||
def verifyLockRefundSpendTx(self, tx_bytes, lock_refund_tx_bytes,
|
def verifySCLockRefundSpendTx(self, tx_bytes, lock_refund_tx_bytes,
|
||||||
lock_refund_tx_id, prevout_script,
|
lock_refund_tx_id, prevout_script,
|
||||||
Kal,
|
Kal,
|
||||||
prevout_n, prevout_value, feerate, vkbv):
|
prevout_n, prevout_value, feerate, vkbv):
|
||||||
lock_refund_spend_tx_obj = self.rpc_callback('decoderawtransaction', [tx_bytes.hex()])
|
lock_refund_spend_tx_obj = self.rpc('decoderawtransaction', [tx_bytes.hex()])
|
||||||
lock_refund_spend_txid_hex = lock_refund_spend_tx_obj['txid']
|
lock_refund_spend_txid_hex = lock_refund_spend_tx_obj['txid']
|
||||||
self._log.info('Verifying lock refund spend tx: {}.'.format(lock_refund_spend_txid_hex))
|
self._log.info('Verifying lock refund spend tx: {}.'.format(lock_refund_spend_txid_hex))
|
||||||
|
|
||||||
@@ -422,10 +452,10 @@ class PARTInterfaceBlind(PARTInterface):
|
|||||||
# Follower is not concerned with them as they pay to leader
|
# Follower is not concerned with them as they pay to leader
|
||||||
|
|
||||||
# Check rangeproofs and commitments sum
|
# Check rangeproofs and commitments sum
|
||||||
lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [lock_refund_tx_bytes.hex()])
|
lock_refund_tx_obj = self.rpc('decoderawtransaction', [lock_refund_tx_bytes.hex()])
|
||||||
prevout = lock_refund_tx_obj['vout'][prevout_n]
|
prevout = lock_refund_tx_obj['vout'][prevout_n]
|
||||||
prevtxns = [{'txid': lock_refund_tx_id.hex(), 'vout': prevout_n, 'scriptPubKey': prevout['scriptPubKey']['hex'], 'amount_commitment': prevout['valueCommitment']}]
|
prevtxns = [{'txid': lock_refund_tx_id.hex(), 'vout': prevout_n, 'scriptPubKey': prevout['scriptPubKey']['hex'], 'amount_commitment': prevout['valueCommitment']}]
|
||||||
rv = self.rpc_callback('verifyrawtransaction', [tx_bytes.hex(), prevtxns])
|
rv = self.rpc('verifyrawtransaction', [tx_bytes.hex(), prevtxns])
|
||||||
ensure(rv['outputs_valid'] is True, 'Invalid outputs')
|
ensure(rv['outputs_valid'] is True, 'Invalid outputs')
|
||||||
ensure(rv['inputs_valid'] is True, 'Invalid inputs')
|
ensure(rv['inputs_valid'] is True, 'Invalid inputs')
|
||||||
|
|
||||||
@@ -440,28 +470,28 @@ class PARTInterfaceBlind(PARTInterface):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def getLockTxSwapOutputValue(self, bid, xmr_swap):
|
def getLockTxSwapOutputValue(self, bid, xmr_swap):
|
||||||
lock_tx_obj = self.rpc_callback('decoderawtransaction', [xmr_swap.a_lock_tx.hex()])
|
lock_tx_obj = self.rpc('decoderawtransaction', [xmr_swap.a_lock_tx.hex()])
|
||||||
nonce = self.getScriptLockTxNonce(xmr_swap.vkbv)
|
nonce = self.getScriptLockTxNonce(xmr_swap.vkbv)
|
||||||
output_n, _ = self.findOutputByNonce(lock_tx_obj, nonce)
|
output_n, _ = self.findOutputByNonce(lock_tx_obj, nonce)
|
||||||
ensure(output_n is not None, 'Output not found in tx')
|
ensure(output_n is not None, 'Output not found in tx')
|
||||||
return bytes.fromhex(lock_tx_obj['vout'][output_n]['valueCommitment'])
|
return bytes.fromhex(lock_tx_obj['vout'][output_n]['valueCommitment'])
|
||||||
|
|
||||||
def getLockRefundTxSwapOutputValue(self, bid, xmr_swap):
|
def getLockRefundTxSwapOutputValue(self, bid, xmr_swap):
|
||||||
lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [xmr_swap.a_lock_refund_tx.hex()])
|
lock_refund_tx_obj = self.rpc('decoderawtransaction', [xmr_swap.a_lock_refund_tx.hex()])
|
||||||
nonce = self.getScriptLockRefundTxNonce(xmr_swap.vkbv)
|
nonce = self.getScriptLockRefundTxNonce(xmr_swap.vkbv)
|
||||||
output_n, _ = self.findOutputByNonce(lock_refund_tx_obj, nonce)
|
output_n, _ = self.findOutputByNonce(lock_refund_tx_obj, nonce)
|
||||||
ensure(output_n is not None, 'Output not found in tx')
|
ensure(output_n is not None, 'Output not found in tx')
|
||||||
return bytes.fromhex(lock_refund_tx_obj['vout'][output_n]['valueCommitment'])
|
return bytes.fromhex(lock_refund_tx_obj['vout'][output_n]['valueCommitment'])
|
||||||
|
|
||||||
def getLockRefundTxSwapOutput(self, xmr_swap):
|
def getLockRefundTxSwapOutput(self, xmr_swap):
|
||||||
lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [xmr_swap.a_lock_refund_tx.hex()])
|
lock_refund_tx_obj = self.rpc('decoderawtransaction', [xmr_swap.a_lock_refund_tx.hex()])
|
||||||
nonce = self.getScriptLockRefundTxNonce(xmr_swap.vkbv)
|
nonce = self.getScriptLockRefundTxNonce(xmr_swap.vkbv)
|
||||||
output_n, _ = self.findOutputByNonce(lock_refund_tx_obj, nonce)
|
output_n, _ = self.findOutputByNonce(lock_refund_tx_obj, nonce)
|
||||||
ensure(output_n is not None, 'Output not found in tx')
|
ensure(output_n is not None, 'Output not found in tx')
|
||||||
return output_n
|
return output_n
|
||||||
|
|
||||||
def createScriptLockSpendTx(self, tx_lock_bytes, script_lock, pk_dest, tx_fee_rate, vkbv):
|
def createSCLockSpendTx(self, tx_lock_bytes: bytes, script_lock: bytes, pk_dest: bytes, tx_fee_rate: int, vkbv: bytes, fee_info={}) -> bytes:
|
||||||
lock_tx_obj = self.rpc_callback('decoderawtransaction', [tx_lock_bytes.hex()])
|
lock_tx_obj = self.rpc('decoderawtransaction', [tx_lock_bytes.hex()])
|
||||||
lock_txid_hex = lock_tx_obj['txid']
|
lock_txid_hex = lock_tx_obj['txid']
|
||||||
|
|
||||||
ensure(lock_tx_obj['version'] == self.txVersion(), 'Bad version')
|
ensure(lock_tx_obj['version'] == self.txVersion(), 'Bad version')
|
||||||
@@ -477,16 +507,16 @@ class PARTInterfaceBlind(PARTInterface):
|
|||||||
inputs = [{'txid': lock_txid_hex, 'vout': spend_n, 'sequence': 0, 'blindingfactor': blinded_info['blind']}]
|
inputs = [{'txid': lock_txid_hex, 'vout': spend_n, 'sequence': 0, 'blindingfactor': blinded_info['blind']}]
|
||||||
outputs = [{'type': 'blind', 'amount': blinded_info['amount'], 'address': addr_out, 'pubkey': pk_dest.hex()}]
|
outputs = [{'type': 'blind', 'amount': blinded_info['amount'], 'address': addr_out, 'pubkey': pk_dest.hex()}]
|
||||||
params = [inputs, outputs]
|
params = [inputs, outputs]
|
||||||
rv = self.rpc_callback('createrawparttransaction', params)
|
rv = self.rpc_wallet('createrawparttransaction', params)
|
||||||
lock_spend_tx_hex = rv['hex']
|
lock_spend_tx_hex = rv['hex']
|
||||||
|
|
||||||
# Set dummy witness data for fee estimation
|
# Set dummy witness data for fee estimation
|
||||||
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
|
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
|
||||||
|
|
||||||
# Use a junk change pubkey to avoid adding unused keys to the wallet
|
# Use a junk change pubkey to avoid adding unused keys to the wallet
|
||||||
zero_change_key = i2b(self.getNewSecretKey())
|
zero_change_key = self.getNewSecretKey()
|
||||||
zero_change_pubkey = self.getPubkey(zero_change_key)
|
zero_change_pubkey = self.getPubkey(zero_change_key)
|
||||||
inputs_info = {'0': {'value': blinded_info['amount'], 'blind': blinded_info['blind'], 'witnessstack': dummy_witness_stack}}
|
inputs_info = {'0': {'value': blinded_info['amount'], 'blind': blinded_info['blind'], 'witnessstack': [x.hex() for x in dummy_witness_stack]}}
|
||||||
outputs_info = rv['amounts']
|
outputs_info = rv['amounts']
|
||||||
options = {
|
options = {
|
||||||
'changepubkey': zero_change_pubkey.hex(),
|
'changepubkey': zero_change_pubkey.hex(),
|
||||||
@@ -494,22 +524,29 @@ class PARTInterfaceBlind(PARTInterface):
|
|||||||
'subtractFeeFromOutputs': [0, ]
|
'subtractFeeFromOutputs': [0, ]
|
||||||
}
|
}
|
||||||
|
|
||||||
rv = self.rpc_callback('fundrawtransactionfrom', ['blind', lock_spend_tx_hex, inputs_info, outputs_info, options])
|
rv = self.rpc_wallet('fundrawtransactionfrom', ['blind', lock_spend_tx_hex, inputs_info, outputs_info, options])
|
||||||
lock_spend_tx_hex = rv['hex']
|
lock_spend_tx_hex = rv['hex']
|
||||||
lock_spend_tx_obj = self.rpc_callback('decoderawtransaction', [lock_spend_tx_hex])
|
lock_spend_tx_obj = self.rpc('decoderawtransaction', [lock_spend_tx_hex])
|
||||||
|
|
||||||
vsize = lock_spend_tx_obj['vsize']
|
|
||||||
pay_fee = make_int(lock_spend_tx_obj['vout'][0]['ct_fee'])
|
pay_fee = make_int(lock_spend_tx_obj['vout'][0]['ct_fee'])
|
||||||
|
|
||||||
|
# lock_spend_tx_hex does not include the dummy witness stack
|
||||||
|
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
|
||||||
|
vsize = self.getTxVSize(self.loadTx(bytes.fromhex(lock_spend_tx_hex)), add_witness_bytes=witness_bytes)
|
||||||
actual_tx_fee_rate = pay_fee * 1000 // vsize
|
actual_tx_fee_rate = pay_fee * 1000 // vsize
|
||||||
self._log.info('createScriptLockSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.',
|
self._log.info('createSCLockSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.',
|
||||||
lock_spend_tx_obj['txid'], actual_tx_fee_rate, vsize, pay_fee)
|
lock_spend_tx_obj['txid'], actual_tx_fee_rate, vsize, pay_fee)
|
||||||
|
|
||||||
|
fee_info['vsize'] = vsize
|
||||||
|
fee_info['fee_paid'] = pay_fee
|
||||||
|
fee_info['rate_input'] = tx_fee_rate
|
||||||
|
fee_info['rate_actual'] = actual_tx_fee_rate
|
||||||
|
|
||||||
return bytes.fromhex(lock_spend_tx_hex)
|
return bytes.fromhex(lock_spend_tx_hex)
|
||||||
|
|
||||||
def verifyLockSpendTx(self, tx_bytes,
|
def verifySCLockSpendTx(self, tx_bytes,
|
||||||
lock_tx_bytes, lock_tx_script,
|
lock_tx_bytes, lock_tx_script,
|
||||||
a_pk_f, feerate, vkbv):
|
a_pk_f, feerate, vkbv):
|
||||||
lock_spend_tx_obj = self.rpc_callback('decoderawtransaction', [tx_bytes.hex()])
|
lock_spend_tx_obj = self.rpc('decoderawtransaction', [tx_bytes.hex()])
|
||||||
lock_spend_txid_hex = lock_spend_tx_obj['txid']
|
lock_spend_txid_hex = lock_spend_tx_obj['txid']
|
||||||
self._log.info('Verifying lock spend tx: {}.'.format(lock_spend_txid_hex))
|
self._log.info('Verifying lock spend tx: {}.'.format(lock_spend_txid_hex))
|
||||||
|
|
||||||
@@ -517,7 +554,7 @@ class PARTInterfaceBlind(PARTInterface):
|
|||||||
ensure(lock_spend_tx_obj['locktime'] == 0, 'Bad nLockTime')
|
ensure(lock_spend_tx_obj['locktime'] == 0, 'Bad nLockTime')
|
||||||
ensure(len(lock_spend_tx_obj['vin']) == 1, 'tx doesn\'t have one input')
|
ensure(len(lock_spend_tx_obj['vin']) == 1, 'tx doesn\'t have one input')
|
||||||
|
|
||||||
lock_tx_obj = self.rpc_callback('decoderawtransaction', [lock_tx_bytes.hex()])
|
lock_tx_obj = self.rpc('decoderawtransaction', [lock_tx_bytes.hex()])
|
||||||
lock_txid_hex = lock_tx_obj['txid']
|
lock_txid_hex = lock_tx_obj['txid']
|
||||||
|
|
||||||
# Find the output of the lock tx to verify
|
# Find the output of the lock tx to verify
|
||||||
@@ -533,7 +570,7 @@ class PARTInterfaceBlind(PARTInterface):
|
|||||||
ensure(len(lock_spend_tx_obj['vout']) == 3, 'tx doesn\'t have three outputs')
|
ensure(len(lock_spend_tx_obj['vout']) == 3, 'tx doesn\'t have three outputs')
|
||||||
|
|
||||||
addr_out = self.pubkey_to_address(a_pk_f)
|
addr_out = self.pubkey_to_address(a_pk_f)
|
||||||
privkey = self.rpc_callback('dumpprivkey', [addr_out])
|
privkey = self.rpc_wallet('dumpprivkey', [addr_out])
|
||||||
|
|
||||||
# Find output:
|
# Find output:
|
||||||
output_blinded_info = None
|
output_blinded_info = None
|
||||||
@@ -542,7 +579,7 @@ class PARTInterfaceBlind(PARTInterface):
|
|||||||
if txo['type'] != 'blind':
|
if txo['type'] != 'blind':
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
output_blinded_info = self.rpc_callback('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], privkey, txo['data_hex']])
|
output_blinded_info = self.rpc('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], privkey, txo['data_hex']])
|
||||||
output_n = txo['n']
|
output_n = txo['n']
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -551,13 +588,13 @@ class PARTInterfaceBlind(PARTInterface):
|
|||||||
ensure(output_n is not None, 'Output not found in tx')
|
ensure(output_n is not None, 'Output not found in tx')
|
||||||
|
|
||||||
# Commitment
|
# Commitment
|
||||||
v = self.rpc_callback('verifycommitment', [lock_spend_tx_obj['vout'][output_n]['valueCommitment'], output_blinded_info['blind'], output_blinded_info['amount']])
|
v = self.rpc('verifycommitment', [lock_spend_tx_obj['vout'][output_n]['valueCommitment'], output_blinded_info['blind'], output_blinded_info['amount']])
|
||||||
ensure(v['result'] is True, 'verifycommitment failed')
|
ensure(v['result'] is True, 'verifycommitment failed')
|
||||||
|
|
||||||
# Check rangeproofs and commitments sum
|
# Check rangeproofs and commitments sum
|
||||||
prevout = lock_tx_obj['vout'][spend_n]
|
prevout = lock_tx_obj['vout'][spend_n]
|
||||||
prevtxns = [{'txid': lock_txid_hex, 'vout': spend_n, 'scriptPubKey': prevout['scriptPubKey']['hex'], 'amount_commitment': prevout['valueCommitment']}]
|
prevtxns = [{'txid': lock_txid_hex, 'vout': spend_n, 'scriptPubKey': prevout['scriptPubKey']['hex'], 'amount_commitment': prevout['valueCommitment']}]
|
||||||
rv = self.rpc_callback('verifyrawtransaction', [tx_bytes.hex(), prevtxns])
|
rv = self.rpc('verifyrawtransaction', [tx_bytes.hex(), prevtxns])
|
||||||
ensure(rv['outputs_valid'] is True, 'Invalid outputs')
|
ensure(rv['outputs_valid'] is True, 'Invalid outputs')
|
||||||
ensure(rv['inputs_valid'] is True, 'Invalid inputs')
|
ensure(rv['inputs_valid'] is True, 'Invalid inputs')
|
||||||
|
|
||||||
@@ -578,10 +615,10 @@ class PARTInterfaceBlind(PARTInterface):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def createScriptLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv):
|
def createSCLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv):
|
||||||
# lock refund swipe tx
|
# lock refund swipe tx
|
||||||
# Sends the coinA locked coin to the follower
|
# Sends the coinA locked coin to the follower
|
||||||
lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [tx_lock_refund_bytes.hex()])
|
lock_refund_tx_obj = self.rpc('decoderawtransaction', [tx_lock_refund_bytes.hex()])
|
||||||
nonce = self.getScriptLockRefundTxNonce(vkbv)
|
nonce = self.getScriptLockRefundTxNonce(vkbv)
|
||||||
|
|
||||||
# Find the output of the lock refund tx to spend
|
# Find the output of the lock refund tx to spend
|
||||||
@@ -590,7 +627,7 @@ class PARTInterfaceBlind(PARTInterface):
|
|||||||
|
|
||||||
tx_lock_refund_id = lock_refund_tx_obj['txid']
|
tx_lock_refund_id = lock_refund_tx_obj['txid']
|
||||||
addr_out = self.pkh_to_address(pkh_dest)
|
addr_out = self.pkh_to_address(pkh_dest)
|
||||||
addr_info = self.rpc_callback('getaddressinfo', [addr_out])
|
addr_info = self.rpc_wallet('getaddressinfo', [addr_out])
|
||||||
output_pubkey_hex = addr_info['pubkey']
|
output_pubkey_hex = addr_info['pubkey']
|
||||||
|
|
||||||
A, B, lock2_value, C = self.extractScriptLockRefundScriptValues(script_lock_refund)
|
A, B, lock2_value, C = self.extractScriptLockRefundScriptValues(script_lock_refund)
|
||||||
@@ -600,15 +637,16 @@ class PARTInterfaceBlind(PARTInterface):
|
|||||||
inputs = [{'txid': tx_lock_refund_id, 'vout': spend_n, 'sequence': lock2_value, 'blindingfactor': input_blinded_info['blind']}]
|
inputs = [{'txid': tx_lock_refund_id, 'vout': spend_n, 'sequence': lock2_value, 'blindingfactor': input_blinded_info['blind']}]
|
||||||
outputs = [{'type': 'blind', 'amount': input_blinded_info['amount'], 'address': addr_out, 'pubkey': output_pubkey_hex}]
|
outputs = [{'type': 'blind', 'amount': input_blinded_info['amount'], 'address': addr_out, 'pubkey': output_pubkey_hex}]
|
||||||
params = [inputs, outputs]
|
params = [inputs, outputs]
|
||||||
rv = self.rpc_callback('createrawparttransaction', params)
|
rv = self.rpc_wallet('createrawparttransaction', params)
|
||||||
|
|
||||||
lock_refund_swipe_tx_hex = rv['hex']
|
lock_refund_swipe_tx_hex = rv['hex']
|
||||||
|
|
||||||
# Set dummy witness data for fee estimation
|
# Set dummy witness data for fee estimation
|
||||||
dummy_witness_stack = self.getScriptLockRefundSwipeTxDummyWitness(script_lock_refund)
|
dummy_witness_stack = self.getScriptLockRefundSwipeTxDummyWitness(script_lock_refund)
|
||||||
|
dummy_witness_stack = [x.hex() for x in dummy_witness_stack]
|
||||||
|
|
||||||
# Use a junk change pubkey to avoid adding unused keys to the wallet
|
# Use a junk change pubkey to avoid adding unused keys to the wallet
|
||||||
zero_change_key = i2b(self.getNewSecretKey())
|
zero_change_key = self.getNewSecretKey()
|
||||||
zero_change_pubkey = self.getPubkey(zero_change_key)
|
zero_change_pubkey = self.getPubkey(zero_change_key)
|
||||||
inputs_info = {'0': {'value': input_blinded_info['amount'], 'blind': input_blinded_info['blind'], 'witnessstack': dummy_witness_stack}}
|
inputs_info = {'0': {'value': input_blinded_info['amount'], 'blind': input_blinded_info['blind'], 'witnessstack': dummy_witness_stack}}
|
||||||
outputs_info = rv['amounts']
|
outputs_info = rv['amounts']
|
||||||
@@ -618,13 +656,129 @@ class PARTInterfaceBlind(PARTInterface):
|
|||||||
'subtractFeeFromOutputs': [0, ]
|
'subtractFeeFromOutputs': [0, ]
|
||||||
}
|
}
|
||||||
|
|
||||||
rv = self.rpc_callback('fundrawtransactionfrom', ['blind', lock_refund_swipe_tx_hex, inputs_info, outputs_info, options])
|
rv = self.rpc_wallet('fundrawtransactionfrom', ['blind', lock_refund_swipe_tx_hex, inputs_info, outputs_info, options])
|
||||||
lock_refund_swipe_tx_hex = rv['hex']
|
lock_refund_swipe_tx_hex = rv['hex']
|
||||||
|
|
||||||
return bytes.fromhex(lock_refund_swipe_tx_hex)
|
return bytes.fromhex(lock_refund_swipe_tx_hex)
|
||||||
|
|
||||||
def getSpendableBalance(self):
|
def getSpendableBalance(self) -> int:
|
||||||
return self.make_int(self.rpc_callback('getbalances')['mine']['blind_trusted'])
|
return self.make_int(self.rpc_wallet('getbalances')['mine']['blind_trusted'])
|
||||||
|
|
||||||
|
def publishBLockTx(self, vkbv: bytes, Kbs: bytes, output_amount: int, feerate: int, delay_for: int = 10, unlock_time: int = 0) -> bytes:
|
||||||
|
Kbv = self.getPubkey(vkbv)
|
||||||
|
sx_addr = self.formatStealthAddress(Kbv, Kbs)
|
||||||
|
self._log.debug('sx_addr: {}'.format(sx_addr))
|
||||||
|
|
||||||
|
# TODO: Fund from other balances
|
||||||
|
params = ['blind', 'blind',
|
||||||
|
[{'address': sx_addr, 'amount': self.format_amount(output_amount)}, ],
|
||||||
|
'', '', self._anon_tx_ring_size, 1, False,
|
||||||
|
{'conf_target': self._conf_target, 'blind_watchonly_visible': True}]
|
||||||
|
|
||||||
|
txid = self.rpc_wallet('sendtypeto', params)
|
||||||
|
return bytes.fromhex(txid)
|
||||||
|
|
||||||
|
def findTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height: int, bid_sender: bool):
|
||||||
|
Kbv = self.getPubkey(kbv)
|
||||||
|
sx_addr = self.formatStealthAddress(Kbv, Kbs)
|
||||||
|
|
||||||
|
# Tx recipient must import the stealth address as watch only
|
||||||
|
if bid_sender:
|
||||||
|
cb_swap_value *= -1
|
||||||
|
else:
|
||||||
|
addr_info = self.rpc_wallet('getaddressinfo', [sx_addr])
|
||||||
|
if not addr_info['iswatchonly']:
|
||||||
|
wif_prefix = self.chainparams_network()['key_prefix']
|
||||||
|
wif_scan_key = toWIF(wif_prefix, kbv)
|
||||||
|
self.rpc_wallet('importstealthaddress', [wif_scan_key, Kbs.hex()])
|
||||||
|
self._log.info('Imported watch-only sx_addr: {}'.format(sx_addr))
|
||||||
|
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height))
|
||||||
|
self.rpc_wallet('rescanblockchain', [restore_height])
|
||||||
|
|
||||||
|
params = [{'include_watchonly': True, 'search': sx_addr}]
|
||||||
|
txns = self.rpc_wallet('filtertransactions', params)
|
||||||
|
|
||||||
|
if len(txns) == 1:
|
||||||
|
tx = txns[0]
|
||||||
|
assert (tx['outputs'][0]['stealth_address'] == sx_addr) # Should not be possible
|
||||||
|
ensure(tx['outputs'][0]['type'] == 'blind', 'Output is not anon')
|
||||||
|
|
||||||
|
if make_int(tx['outputs'][0]['amount']) == cb_swap_value:
|
||||||
|
height = 0
|
||||||
|
if tx['confirmations'] > 0:
|
||||||
|
chain_height = self.rpc('getblockcount')
|
||||||
|
height = chain_height - (tx['confirmations'] - 1)
|
||||||
|
return {'txid': tx['txid'], 'amount': cb_swap_value, 'height': height}
|
||||||
|
else:
|
||||||
|
self._log.warning('Incorrect amount detected for coin b lock txn: {}'.format(tx['txid']))
|
||||||
|
return -1
|
||||||
|
return None
|
||||||
|
|
||||||
|
def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee: int, restore_height: int, spend_actual_balance: bool = False) -> bytes:
|
||||||
|
Kbv = self.getPubkey(kbv)
|
||||||
|
Kbs = self.getPubkey(kbs)
|
||||||
|
sx_addr = self.formatStealthAddress(Kbv, Kbs)
|
||||||
|
addr_info = self.rpc_wallet('getaddressinfo', [sx_addr])
|
||||||
|
if not addr_info['ismine']:
|
||||||
|
wif_prefix = self.chainparams_network()['key_prefix']
|
||||||
|
wif_scan_key = toWIF(wif_prefix, kbv)
|
||||||
|
wif_spend_key = toWIF(wif_prefix, kbs)
|
||||||
|
self.rpc_wallet('importstealthaddress', [wif_scan_key, wif_spend_key])
|
||||||
|
self._log.info('Imported spend key for sx_addr: {}'.format(sx_addr))
|
||||||
|
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height))
|
||||||
|
self.rpc_wallet('rescanblockchain', [restore_height])
|
||||||
|
|
||||||
|
# TODO: Remove workaround
|
||||||
|
# utxos = self.rpc_wallet('listunspentblind', [1, 9999999, [sx_addr]])
|
||||||
|
utxos = []
|
||||||
|
all_utxos = self.rpc_wallet('listunspentblind', [1, 9999999])
|
||||||
|
for utxo in all_utxos:
|
||||||
|
if utxo.get('stealth_address', '_') == sx_addr:
|
||||||
|
utxos.append(utxo)
|
||||||
|
if len(utxos) < 1:
|
||||||
|
raise TemporaryError('No spendable outputs')
|
||||||
|
elif len(utxos) > 1:
|
||||||
|
raise ValueError('Too many spendable outputs')
|
||||||
|
|
||||||
|
utxo = utxos[0]
|
||||||
|
utxo_sats = make_int(utxo['amount'])
|
||||||
|
|
||||||
|
if spend_actual_balance and utxo_sats != cb_swap_value:
|
||||||
|
self._log.warning('Spending actual balance {}, not swap value {}.'.format(utxo_sats, cb_swap_value))
|
||||||
|
cb_swap_value = utxo_sats
|
||||||
|
|
||||||
|
inputs = [{'tx': utxo['txid'], 'n': utxo['vout']}, ]
|
||||||
|
params = ['blind', 'blind',
|
||||||
|
[{'address': address_to, 'amount': self.format_amount(cb_swap_value), 'subfee': True}, ],
|
||||||
|
'', '', self._anon_tx_ring_size, 1, False,
|
||||||
|
{'conf_target': self._conf_target, 'inputs': inputs, 'show_fee': True}]
|
||||||
|
rv = self.rpc_wallet('sendtypeto', params)
|
||||||
|
return bytes.fromhex(rv['txid'])
|
||||||
|
|
||||||
|
def findTxnByHash(self, txid_hex):
|
||||||
|
# txindex is enabled for Particl
|
||||||
|
|
||||||
|
try:
|
||||||
|
rv = self.rpc('getrawtransaction', [txid_hex, True])
|
||||||
|
except Exception as ex:
|
||||||
|
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
|
||||||
|
return None
|
||||||
|
|
||||||
|
if 'confirmations' in rv and rv['confirmations'] >= self.blocks_confirmed:
|
||||||
|
return {'txid': txid_hex, 'amount': 0, 'height': rv['height']}
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def createRawFundedTransaction(self, addr_to: str, amount: int, sub_fee: bool = False, lock_unspents: bool = True) -> str:
|
||||||
|
txn = self.rpc_wallet('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
|
||||||
|
|
||||||
|
options = {
|
||||||
|
'lockUnspents': lock_unspents,
|
||||||
|
'conf_target': self._conf_target,
|
||||||
|
}
|
||||||
|
if sub_fee:
|
||||||
|
options['subtractFeeFromOutputs'] = [0,]
|
||||||
|
return self.rpc_wallet('fundrawtransactionfrom', ['blind', txn, options])['hex']
|
||||||
|
|
||||||
|
|
||||||
class PARTInterfaceAnon(PARTInterface):
|
class PARTInterfaceAnon(PARTInterface):
|
||||||
@@ -632,16 +786,25 @@ class PARTInterfaceAnon(PARTInterface):
|
|||||||
def balance_type():
|
def balance_type():
|
||||||
return BalanceTypes.ANON
|
return BalanceTypes.ANON
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def xmr_swap_a_lock_spend_tx_vsize() -> int:
|
||||||
|
raise ValueError('Not possible')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def xmr_swap_b_lock_spend_tx_vsize() -> int:
|
||||||
|
# TODO: Estimate with ringsize
|
||||||
|
return 1153
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def depth_spendable() -> int:
|
def depth_spendable() -> int:
|
||||||
return 12
|
return 12
|
||||||
|
|
||||||
def coin_name(self):
|
def coin_name(self) -> str:
|
||||||
return super().coin_name() + ' Anon'
|
return super().coin_name() + ' Anon'
|
||||||
|
|
||||||
def publishBLockTx(self, Kbv, Kbs, output_amount, feerate):
|
def publishBLockTx(self, kbv: bytes, Kbs: bytes, output_amount: int, feerate: int, delay_for: int = 10, unlock_time: int = 0) -> bytes:
|
||||||
|
Kbv = self.getPubkey(kbv)
|
||||||
sx_addr = self.formatStealthAddress(Kbv, Kbs)
|
sx_addr = self.formatStealthAddress(Kbv, Kbs)
|
||||||
self._log.debug('sx_addr: {}'.format(sx_addr))
|
|
||||||
|
|
||||||
# TODO: Fund from other balances
|
# TODO: Fund from other balances
|
||||||
params = ['anon', 'anon',
|
params = ['anon', 'anon',
|
||||||
@@ -649,7 +812,7 @@ class PARTInterfaceAnon(PARTInterface):
|
|||||||
'', '', self._anon_tx_ring_size, 1, False,
|
'', '', self._anon_tx_ring_size, 1, False,
|
||||||
{'conf_target': self._conf_target, 'blind_watchonly_visible': True}]
|
{'conf_target': self._conf_target, 'blind_watchonly_visible': True}]
|
||||||
|
|
||||||
txid = self.rpc_callback('sendtypeto', params)
|
txid = self.rpc_wallet('sendtypeto', params)
|
||||||
return bytes.fromhex(txid)
|
return bytes.fromhex(txid)
|
||||||
|
|
||||||
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):
|
||||||
@@ -661,17 +824,17 @@ class PARTInterfaceAnon(PARTInterface):
|
|||||||
if bid_sender:
|
if bid_sender:
|
||||||
cb_swap_value *= -1
|
cb_swap_value *= -1
|
||||||
else:
|
else:
|
||||||
addr_info = self.rpc_callback('getaddressinfo', [sx_addr])
|
addr_info = self.rpc_wallet('getaddressinfo', [sx_addr])
|
||||||
if not addr_info['iswatchonly']:
|
if not addr_info['iswatchonly']:
|
||||||
wif_prefix = self.chainparams_network()['key_prefix']
|
wif_prefix = self.chainparams_network()['key_prefix']
|
||||||
wif_scan_key = toWIF(wif_prefix, kbv)
|
wif_scan_key = toWIF(wif_prefix, kbv)
|
||||||
self.rpc_callback('importstealthaddress', [wif_scan_key, Kbs.hex()])
|
self.rpc_wallet('importstealthaddress', [wif_scan_key, Kbs.hex()])
|
||||||
self._log.info('Imported watch-only sx_addr: {}'.format(sx_addr))
|
self._log.info('Imported watch-only sx_addr: {}'.format(sx_addr))
|
||||||
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height))
|
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height))
|
||||||
self.rpc_callback('rescanblockchain', [restore_height])
|
self.rpc_wallet('rescanblockchain', [restore_height])
|
||||||
|
|
||||||
params = [{'include_watchonly': True, 'search': sx_addr}]
|
params = [{'include_watchonly': True, 'search': sx_addr}]
|
||||||
txns = self.rpc_callback('filtertransactions', params)
|
txns = self.rpc_wallet('filtertransactions', params)
|
||||||
|
|
||||||
if len(txns) == 1:
|
if len(txns) == 1:
|
||||||
tx = txns[0]
|
tx = txns[0]
|
||||||
@@ -681,7 +844,7 @@ class PARTInterfaceAnon(PARTInterface):
|
|||||||
if make_int(tx['outputs'][0]['amount']) == cb_swap_value:
|
if make_int(tx['outputs'][0]['amount']) == cb_swap_value:
|
||||||
height = 0
|
height = 0
|
||||||
if tx['confirmations'] > 0:
|
if tx['confirmations'] > 0:
|
||||||
chain_height = self.rpc_callback('getblockcount')
|
chain_height = self.rpc('getblockcount')
|
||||||
height = chain_height - (tx['confirmations'] - 1)
|
height = chain_height - (tx['confirmations'] - 1)
|
||||||
return {'txid': tx['txid'], 'amount': cb_swap_value, 'height': height}
|
return {'txid': tx['txid'], 'amount': cb_swap_value, 'height': height}
|
||||||
else:
|
else:
|
||||||
@@ -689,21 +852,21 @@ class PARTInterfaceAnon(PARTInterface):
|
|||||||
return -1
|
return -1
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def spendBLockTx(self, chain_b_lock_txid, address_to, kbv, kbs, cb_swap_value, b_fee, restore_height, spend_actual_balance=False):
|
def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee: int, restore_height: int, spend_actual_balance: bool = False) -> bytes:
|
||||||
Kbv = self.getPubkey(kbv)
|
Kbv = self.getPubkey(kbv)
|
||||||
Kbs = self.getPubkey(kbs)
|
Kbs = self.getPubkey(kbs)
|
||||||
sx_addr = self.formatStealthAddress(Kbv, Kbs)
|
sx_addr = self.formatStealthAddress(Kbv, Kbs)
|
||||||
addr_info = self.rpc_callback('getaddressinfo', [sx_addr])
|
addr_info = self.rpc_wallet('getaddressinfo', [sx_addr])
|
||||||
if not addr_info['ismine']:
|
if not addr_info['ismine']:
|
||||||
wif_prefix = self.chainparams_network()['key_prefix']
|
wif_prefix = self.chainparams_network()['key_prefix']
|
||||||
wif_scan_key = toWIF(wif_prefix, kbv)
|
wif_scan_key = toWIF(wif_prefix, kbv)
|
||||||
wif_spend_key = toWIF(wif_prefix, kbs)
|
wif_spend_key = toWIF(wif_prefix, kbs)
|
||||||
self.rpc_callback('importstealthaddress', [wif_scan_key, wif_spend_key])
|
self.rpc_wallet('importstealthaddress', [wif_scan_key, wif_spend_key])
|
||||||
self._log.info('Imported spend key for sx_addr: {}'.format(sx_addr))
|
self._log.info('Imported spend key for sx_addr: {}'.format(sx_addr))
|
||||||
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height))
|
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height))
|
||||||
self.rpc_callback('rescanblockchain', [restore_height])
|
self.rpc_wallet('rescanblockchain', [restore_height])
|
||||||
|
|
||||||
autxos = self.rpc_callback('listunspentanon', [1, 9999999, [sx_addr]])
|
autxos = self.rpc_wallet('listunspentanon', [1, 9999999, [sx_addr]])
|
||||||
|
|
||||||
if len(autxos) < 1:
|
if len(autxos) < 1:
|
||||||
raise TemporaryError('No spendable outputs')
|
raise TemporaryError('No spendable outputs')
|
||||||
@@ -722,14 +885,14 @@ class PARTInterfaceAnon(PARTInterface):
|
|||||||
[{'address': address_to, 'amount': self.format_amount(cb_swap_value), 'subfee': True}, ],
|
[{'address': address_to, 'amount': self.format_amount(cb_swap_value), 'subfee': True}, ],
|
||||||
'', '', self._anon_tx_ring_size, 1, False,
|
'', '', self._anon_tx_ring_size, 1, False,
|
||||||
{'conf_target': self._conf_target, 'inputs': inputs, 'show_fee': True}]
|
{'conf_target': self._conf_target, 'inputs': inputs, 'show_fee': True}]
|
||||||
rv = self.rpc_callback('sendtypeto', params)
|
rv = self.rpc_wallet('sendtypeto', params)
|
||||||
return bytes.fromhex(rv['txid'])
|
return bytes.fromhex(rv['txid'])
|
||||||
|
|
||||||
def findTxnByHash(self, txid_hex):
|
def findTxnByHash(self, txid_hex: str):
|
||||||
# txindex is enabled for Particl
|
# txindex is enabled for Particl
|
||||||
|
|
||||||
try:
|
try:
|
||||||
rv = self.rpc_callback('getrawtransaction', [txid_hex, True])
|
rv = self.rpc('getrawtransaction', [txid_hex, True])
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
|
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
|
||||||
return None
|
return None
|
||||||
@@ -739,5 +902,5 @@ class PARTInterfaceAnon(PARTInterface):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def getSpendableBalance(self):
|
def getSpendableBalance(self) -> int:
|
||||||
return self.make_int(self.rpc_callback('getbalances')['mine']['anon_trusted'])
|
return self.make_int(self.rpc_wallet('getbalances')['mine']['anon_trusted'])
|
||||||
|
|||||||
126
basicswap/interface/pivx.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2022 tecnovert
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from .btc import BTCInterface
|
||||||
|
from basicswap.rpc import make_rpc_func
|
||||||
|
from basicswap.chainparams import Coins
|
||||||
|
from basicswap.util.address import decodeAddress
|
||||||
|
from .contrib.pivx_test_framework.messages import (
|
||||||
|
CBlock,
|
||||||
|
ToHex,
|
||||||
|
FromHex,
|
||||||
|
CTransaction)
|
||||||
|
from basicswap.contrib.test_framework.script import (
|
||||||
|
CScript,
|
||||||
|
OP_DUP,
|
||||||
|
OP_HASH160,
|
||||||
|
OP_CHECKSIG,
|
||||||
|
OP_EQUALVERIFY,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PIVXInterface(BTCInterface):
|
||||||
|
@staticmethod
|
||||||
|
def coin_type():
|
||||||
|
return Coins.PIVX
|
||||||
|
|
||||||
|
def __init__(self, coin_settings, network, swap_client=None):
|
||||||
|
super(PIVXInterface, self).__init__(coin_settings, network, swap_client)
|
||||||
|
# No multiwallet support
|
||||||
|
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host)
|
||||||
|
|
||||||
|
def checkWallets(self) -> int:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def signTxWithWallet(self, tx):
|
||||||
|
rv = self.rpc('signrawtransaction', [tx.hex()])
|
||||||
|
return bytes.fromhex(rv['hex'])
|
||||||
|
|
||||||
|
def createRawFundedTransaction(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)
|
||||||
|
self._log.debug(f'Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}')
|
||||||
|
options = {
|
||||||
|
'lockUnspents': lock_unspents,
|
||||||
|
'feeRate': fee_rate,
|
||||||
|
}
|
||||||
|
if sub_fee:
|
||||||
|
options['subtractFeeFromOutputs'] = [0,]
|
||||||
|
return self.rpc('fundrawtransaction', [txn, options])['hex']
|
||||||
|
|
||||||
|
def createRawSignedTransaction(self, addr_to, amount) -> str:
|
||||||
|
txn_funded = self.createRawFundedTransaction(addr_to, amount)
|
||||||
|
return self.rpc('signrawtransaction', [txn_funded])['hex']
|
||||||
|
|
||||||
|
def decodeAddress(self, address):
|
||||||
|
return decodeAddress(address)[1:]
|
||||||
|
|
||||||
|
def getBlockWithTxns(self, block_hash):
|
||||||
|
# TODO: Bypass decoderawtransaction and getblockheader
|
||||||
|
block = self.rpc('getblock', [block_hash, False])
|
||||||
|
block_header = self.rpc('getblockheader', [block_hash])
|
||||||
|
decoded_block = CBlock()
|
||||||
|
decoded_block = FromHex(decoded_block, block)
|
||||||
|
|
||||||
|
tx_rv = []
|
||||||
|
for tx in decoded_block.vtx:
|
||||||
|
tx_dec = self.rpc('decoderawtransaction', [ToHex(tx)])
|
||||||
|
tx_rv.append(tx_dec)
|
||||||
|
|
||||||
|
block_rv = {
|
||||||
|
'hash': block_hash,
|
||||||
|
'tx': tx_rv,
|
||||||
|
'confirmations': block_header['confirmations'],
|
||||||
|
'height': block_header['height'],
|
||||||
|
'version': block_header['version'],
|
||||||
|
'merkleroot': block_header['merkleroot'],
|
||||||
|
}
|
||||||
|
|
||||||
|
return block_rv
|
||||||
|
|
||||||
|
def withdrawCoin(self, value, addr_to, subfee):
|
||||||
|
params = [addr_to, value, '', '', subfee]
|
||||||
|
return self.rpc('sendtoaddress', params)
|
||||||
|
|
||||||
|
def getSpendableBalance(self) -> int:
|
||||||
|
return self.make_int(self.rpc('getwalletinfo')['balance'])
|
||||||
|
|
||||||
|
def loadTx(self, tx_bytes):
|
||||||
|
# Load tx from bytes to internal representation
|
||||||
|
tx = CTransaction()
|
||||||
|
tx.deserialize(BytesIO(tx_bytes))
|
||||||
|
return tx
|
||||||
|
|
||||||
|
def getScriptForPubkeyHash(self, pkh: bytes) -> bytearray:
|
||||||
|
# Return P2PKH
|
||||||
|
return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG])
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def signTxWithKey(self, tx: bytes, key: bytes) -> bytes:
|
||||||
|
key_wif = self.encodeKey(key)
|
||||||
|
rv = self.rpc('signrawtransaction', [tx.hex(), [], [key_wif, ]])
|
||||||
|
return bytes.fromhex(rv['hex'])
|
||||||
|
|
||||||
|
def findTxnByHash(self, txid_hex: str):
|
||||||
|
# Only works for wallet txns
|
||||||
|
try:
|
||||||
|
rv = self.rpc('gettransaction', [txid_hex])
|
||||||
|
except Exception as ex:
|
||||||
|
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
|
||||||
|
return None
|
||||||
|
if 'confirmations' in rv and rv['confirmations'] >= self.blocks_confirmed:
|
||||||
|
block_height = self.getBlockHeader(rv['blockhash'])['height']
|
||||||
|
return {'txid': txid_hex, 'amount': 0, 'height': block_height}
|
||||||
|
return None
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2020-2022 tecnovert
|
# Copyright (c) 2020-2024 tecnovert
|
||||||
# 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.
|
||||||
|
|
||||||
@@ -24,21 +24,27 @@ from coincurve.dleag import (
|
|||||||
verify_ed25519_point,
|
verify_ed25519_point,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from basicswap.interface import (
|
||||||
|
Curves)
|
||||||
from basicswap.util import (
|
from basicswap.util import (
|
||||||
|
i2b, b2i, b2h,
|
||||||
dumpj,
|
dumpj,
|
||||||
ensure,
|
ensure,
|
||||||
make_int,
|
make_int,
|
||||||
TemporaryError)
|
TemporaryError)
|
||||||
|
from basicswap.util.network import (
|
||||||
|
is_private_ip_address)
|
||||||
from basicswap.rpc_xmr import (
|
from basicswap.rpc_xmr import (
|
||||||
make_xmr_rpc_func,
|
make_xmr_rpc_func,
|
||||||
make_xmr_rpc2_func,
|
make_xmr_rpc2_func)
|
||||||
make_xmr_wallet_rpc_func)
|
|
||||||
from basicswap.util import (
|
|
||||||
b2i, b2h)
|
|
||||||
from basicswap.chainparams import XMR_COIN, CoinInterface, Coins
|
from basicswap.chainparams import XMR_COIN, CoinInterface, Coins
|
||||||
|
|
||||||
|
|
||||||
class XMRInterface(CoinInterface):
|
class XMRInterface(CoinInterface):
|
||||||
|
@staticmethod
|
||||||
|
def curve_type():
|
||||||
|
return Curves.ed25519
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def coin_type():
|
def coin_type():
|
||||||
return Coins.XMR
|
return Coins.XMR
|
||||||
@@ -63,17 +69,65 @@ class XMRInterface(CoinInterface):
|
|||||||
def depth_spendable() -> int:
|
def depth_spendable() -> int:
|
||||||
return 10
|
return 10
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def xmr_swap_a_lock_spend_tx_vsize() -> int:
|
||||||
|
raise ValueError('Not possible')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def xmr_swap_b_lock_spend_tx_vsize() -> int:
|
||||||
|
# TODO: Estimate with ringsize
|
||||||
|
return 1507
|
||||||
|
|
||||||
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.rpc_cb = make_xmr_rpc_func(coin_settings['rpcport'], host=coin_settings.get('rpchost', '127.0.0.1'))
|
|
||||||
self.rpc_cb2 = make_xmr_rpc2_func(coin_settings['rpcport'], host=coin_settings.get('rpchost', '127.0.0.1')) # non-json endpoint
|
|
||||||
self.rpc_wallet_cb = make_xmr_wallet_rpc_func(coin_settings['walletrpcport'], coin_settings['walletrpcauth'], host=coin_settings.get('walletrpchost', '127.0.0.1'))
|
|
||||||
|
|
||||||
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._have_checked_seed = False
|
||||||
|
|
||||||
|
daemon_login = None
|
||||||
|
if coin_settings.get('rpcuser', '') != '':
|
||||||
|
daemon_login = (coin_settings.get('rpcuser', ''), coin_settings.get('rpcpassword', ''))
|
||||||
|
|
||||||
|
rpchost = coin_settings.get('rpchost', '127.0.0.1')
|
||||||
|
proxy_host = None
|
||||||
|
proxy_port = None
|
||||||
|
# Connect to the daemon over a proxy if not running locally
|
||||||
|
if swap_client:
|
||||||
|
chain_client_settings = swap_client.getChainClientSettings(self.coin_type())
|
||||||
|
manage_daemon: bool = chain_client_settings['manage_daemon']
|
||||||
|
if swap_client.use_tor_proxy:
|
||||||
|
if manage_daemon is False:
|
||||||
|
log_str: str = ''
|
||||||
|
have_cc_tor_opt = 'use_tor' in chain_client_settings
|
||||||
|
if have_cc_tor_opt and chain_client_settings['use_tor'] is False:
|
||||||
|
log_str = ' bypassing proxy (use_tor false for XMR)'
|
||||||
|
elif have_cc_tor_opt is False and is_private_ip_address(rpchost):
|
||||||
|
log_str = ' bypassing proxy (private ip address)'
|
||||||
|
else:
|
||||||
|
proxy_host = swap_client.tor_proxy_host
|
||||||
|
proxy_port = swap_client.tor_proxy_port
|
||||||
|
log_str = f' through proxy at {proxy_host}'
|
||||||
|
self._log.info(f'Connecting to remote {self.coin_name()} daemon at {rpchost}{log_str}.')
|
||||||
|
else:
|
||||||
|
self._log.info(f'Not connecting to local {self.coin_name()} daemon through proxy.')
|
||||||
|
elif manage_daemon is False:
|
||||||
|
self._log.info(f'Connecting to remote {self.coin_name()} daemon at {rpchost}.')
|
||||||
|
|
||||||
|
self._rpctimeout = coin_settings.get('rpctimeout', 60)
|
||||||
|
self._walletrpctimeout = coin_settings.get('walletrpctimeout', 120)
|
||||||
|
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.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 checkWallets(self) -> int:
|
||||||
|
return 1
|
||||||
|
|
||||||
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')
|
||||||
@@ -82,10 +136,28 @@ class XMRInterface(CoinInterface):
|
|||||||
def setWalletFilename(self, wallet_filename):
|
def setWalletFilename(self, wallet_filename):
|
||||||
self._wallet_filename = wallet_filename
|
self._wallet_filename = wallet_filename
|
||||||
|
|
||||||
|
def createWallet(self, params):
|
||||||
|
if self._wallet_password is not None:
|
||||||
|
params['password'] = self._wallet_password
|
||||||
|
rv = self.rpc_wallet('generate_from_keys', params)
|
||||||
|
self._log.info('generate_from_keys %s', dumpj(rv))
|
||||||
|
|
||||||
|
def openWallet(self, filename):
|
||||||
|
params = {'filename': filename}
|
||||||
|
if self._wallet_password is not None:
|
||||||
|
params['password'] = self._wallet_password
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Can't reopen the same wallet in windows, !is_keys_file_locked()
|
||||||
|
self.rpc_wallet('close_wallet')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.rpc_wallet('open_wallet', params)
|
||||||
|
|
||||||
def initialiseWallet(self, key_view, key_spend, restore_height=None):
|
def initialiseWallet(self, key_view, key_spend, restore_height=None):
|
||||||
with self._mx_wallet:
|
with self._mx_wallet:
|
||||||
try:
|
try:
|
||||||
self.rpc_wallet_cb('open_wallet', {'filename': 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:
|
||||||
@@ -102,22 +174,21 @@ class XMRInterface(CoinInterface):
|
|||||||
'spendkey': b2h(key_spend[::-1]),
|
'spendkey': b2h(key_spend[::-1]),
|
||||||
'restore_height': self._restore_height,
|
'restore_height': self._restore_height,
|
||||||
}
|
}
|
||||||
rv = self.rpc_wallet_cb('generate_from_keys', params)
|
self.createWallet(params)
|
||||||
self._log.info('generate_from_keys %s', dumpj(rv))
|
self.openWallet(self._wallet_filename)
|
||||||
self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename})
|
|
||||||
|
|
||||||
def ensureWalletExists(self):
|
def ensureWalletExists(self) -> None:
|
||||||
with self._mx_wallet:
|
with self._mx_wallet:
|
||||||
self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename})
|
self.openWallet(self._wallet_filename)
|
||||||
|
|
||||||
def testDaemonRPC(self, with_wallet=True):
|
def testDaemonRPC(self, with_wallet=True) -> None:
|
||||||
self.rpc_wallet_cb('get_languages')
|
self.rpc_wallet('get_languages')
|
||||||
|
|
||||||
def getDaemonVersion(self):
|
def getDaemonVersion(self):
|
||||||
return self.rpc_wallet_cb('get_version')['version']
|
return self.rpc_wallet('get_version')['version']
|
||||||
|
|
||||||
def getBlockchainInfo(self):
|
def getBlockchainInfo(self):
|
||||||
get_height = self.rpc_cb2('get_height', timeout=30)
|
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,
|
||||||
@@ -128,7 +199,7 @@ class XMRInterface(CoinInterface):
|
|||||||
# 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.rpc_cb2('get_info', timeout=30)
|
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']
|
||||||
|
|
||||||
@@ -136,7 +207,7 @@ class XMRInterface(CoinInterface):
|
|||||||
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_cb('get_block_count', timeout=30)['count']
|
rv['known_block_count'] = self.rpc('get_block_count', timeout=self._rpctimeout)['count']
|
||||||
rv['verificationprogress'] = rv['blocks'] / rv['known_block_count']
|
rv['verificationprogress'] = rv['blocks'] / rv['known_block_count']
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._log.warning('XMR get_block_count failed with: %s', str(e))
|
self._log.warning('XMR get_block_count failed with: %s', str(e))
|
||||||
@@ -145,45 +216,60 @@ class XMRInterface(CoinInterface):
|
|||||||
return rv
|
return rv
|
||||||
|
|
||||||
def getChainHeight(self):
|
def getChainHeight(self):
|
||||||
return self.rpc_cb2('get_height', timeout=30)['height']
|
return self.rpc2('get_height', timeout=self._rpctimeout)['height']
|
||||||
|
|
||||||
def getWalletInfo(self):
|
def getWalletInfo(self):
|
||||||
with self._mx_wallet:
|
with self._mx_wallet:
|
||||||
self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename})
|
try:
|
||||||
|
self.openWallet(self._wallet_filename)
|
||||||
|
except Exception as e:
|
||||||
|
if 'Failed to open wallet' in str(e):
|
||||||
|
rv = {'encrypted': True, 'locked': True, 'balance': 0, 'unconfirmed_balance': 0}
|
||||||
|
return rv
|
||||||
|
raise e
|
||||||
|
|
||||||
rv = {}
|
rv = {}
|
||||||
self.rpc_wallet_cb('refresh')
|
self.rpc_wallet('refresh')
|
||||||
balance_info = self.rpc_wallet_cb('get_balance')
|
balance_info = self.rpc_wallet('get_balance')
|
||||||
rv['balance'] = self.format_amount(balance_info['unlocked_balance'])
|
rv['balance'] = self.format_amount(balance_info['unlocked_balance'])
|
||||||
rv['unconfirmed_balance'] = self.format_amount(balance_info['balance'] - balance_info['unlocked_balance'])
|
rv['unconfirmed_balance'] = self.format_amount(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 walletRestoreHeight(self):
|
def walletRestoreHeight(self):
|
||||||
return self._restore_height
|
return self._restore_height
|
||||||
|
|
||||||
def getMainWalletAddress(self):
|
def getMainWalletAddress(self) -> str:
|
||||||
with self._mx_wallet:
|
with self._mx_wallet:
|
||||||
self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename})
|
self.openWallet(self._wallet_filename)
|
||||||
return self.rpc_wallet_cb('get_address')['address']
|
return self.rpc_wallet('get_address')['address']
|
||||||
|
|
||||||
def getNewAddress(self, placeholder):
|
def getNewAddress(self, placeholder) -> str:
|
||||||
with self._mx_wallet:
|
with self._mx_wallet:
|
||||||
self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename})
|
self.openWallet(self._wallet_filename)
|
||||||
return self.rpc_wallet_cb('create_address', {'account_index': 0})['address']
|
new_address = self.rpc_wallet('create_address', {'account_index': 0})['address']
|
||||||
|
self.rpc_wallet('store')
|
||||||
|
return new_address
|
||||||
|
|
||||||
def get_fee_rate(self, conf_target=2):
|
def get_fee_rate(self, conf_target: int = 2):
|
||||||
self._log.warning('TODO - estimate fee rate?')
|
self._log.warning('TODO - estimate XMR fee rate?')
|
||||||
return 0.0, 'unused'
|
return 0.0, 'unused'
|
||||||
|
|
||||||
def getNewSecretKey(self):
|
def getNewSecretKey(self) -> bytes:
|
||||||
return edu.get_secret()
|
# Note: Returned bytes are in big endian order
|
||||||
|
return i2b(edu.get_secret())
|
||||||
|
|
||||||
def pubkey(self, key):
|
def pubkey(self, key: bytes) -> bytes:
|
||||||
return edf.scalarmult_B(key)
|
return edf.scalarmult_B(key)
|
||||||
|
|
||||||
def encodeKey(self, vk):
|
def encodeKey(self, vk: bytes) -> str:
|
||||||
return vk.hex()
|
return vk[::-1].hex()
|
||||||
|
|
||||||
def encodePubkey(self, pk):
|
def decodeKey(self, k_hex: str) -> bytes:
|
||||||
|
return bytes.fromhex(k_hex)[::-1]
|
||||||
|
|
||||||
|
def encodePubkey(self, pk: bytes) -> str:
|
||||||
return edu.encodepoint(pk)
|
return edu.encodepoint(pk)
|
||||||
|
|
||||||
def decodePubkey(self, pke):
|
def decodePubkey(self, pke):
|
||||||
@@ -192,12 +278,12 @@ class XMRInterface(CoinInterface):
|
|||||||
def getPubkey(self, privkey):
|
def getPubkey(self, privkey):
|
||||||
return ed25519_get_pubkey(privkey)
|
return ed25519_get_pubkey(privkey)
|
||||||
|
|
||||||
def getAddressFromKeys(self, key_view, key_spend):
|
def getAddressFromKeys(self, key_view: bytes, key_spend: bytes) -> str:
|
||||||
pk_view = self.getPubkey(key_view)
|
pk_view = self.getPubkey(key_view)
|
||||||
pk_spend = self.getPubkey(key_spend)
|
pk_spend = self.getPubkey(key_spend)
|
||||||
return xmr_util.encode_address(pk_view, pk_spend)
|
return xmr_util.encode_address(pk_view, pk_spend)
|
||||||
|
|
||||||
def verifyKey(self, k):
|
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)
|
||||||
|
|
||||||
@@ -206,46 +292,45 @@ class XMRInterface(CoinInterface):
|
|||||||
# Checks for small order
|
# Checks for small order
|
||||||
return verify_ed25519_point(pubkey_bytes)
|
return verify_ed25519_point(pubkey_bytes)
|
||||||
|
|
||||||
def proveDLEAG(self, key):
|
def proveDLEAG(self, key: bytes) -> bytes:
|
||||||
privkey = PrivateKey(key)
|
privkey = PrivateKey(key)
|
||||||
return dleag_prove(privkey)
|
return dleag_prove(privkey)
|
||||||
|
|
||||||
def verifyDLEAG(self, dleag_bytes):
|
def verifyDLEAG(self, dleag_bytes: bytes) -> bool:
|
||||||
return dleag_verify(dleag_bytes)
|
return dleag_verify(dleag_bytes)
|
||||||
|
|
||||||
def lengthDLEAG(self):
|
def lengthDLEAG(self) -> int:
|
||||||
return dleag_proof_len()
|
return dleag_proof_len()
|
||||||
|
|
||||||
def decodeKey(self, k_hex):
|
def sumKeys(self, ka: bytes, kb: bytes) -> bytes:
|
||||||
return bytes.fromhex(k_hex)
|
|
||||||
|
|
||||||
def sumKeys(self, ka, kb):
|
|
||||||
return ed25519_scalar_add(ka, kb)
|
return ed25519_scalar_add(ka, kb)
|
||||||
|
|
||||||
def sumPubkeys(self, Ka, Kb):
|
def sumPubkeys(self, Ka: bytes, Kb: bytes) -> bytes:
|
||||||
return ed25519_add(Ka, Kb)
|
return ed25519_add(Ka, Kb)
|
||||||
|
|
||||||
def encodeSharedAddress(self, Kbv, Kbs):
|
def encodeSharedAddress(self, Kbv: bytes, Kbs: bytes) -> str:
|
||||||
return xmr_util.encode_address(Kbv, Kbs)
|
return xmr_util.encode_address(Kbv, Kbs)
|
||||||
|
|
||||||
def publishBLockTx(self, Kbv, Kbs, output_amount, feerate, delay_for=10):
|
def publishBLockTx(self, kbv: bytes, Kbs: bytes, output_amount: int, feerate: int, delay_for: int = 10, unlock_time: int = 0) -> bytes:
|
||||||
with self._mx_wallet:
|
with self._mx_wallet:
|
||||||
self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename})
|
self.openWallet(self._wallet_filename)
|
||||||
|
self.rpc_wallet('refresh')
|
||||||
|
|
||||||
|
Kbv = self.getPubkey(kbv)
|
||||||
shared_addr = xmr_util.encode_address(Kbv, Kbs)
|
shared_addr = xmr_util.encode_address(Kbv, Kbs)
|
||||||
|
|
||||||
params = {'destinations': [{'amount': output_amount, 'address': shared_addr}]}
|
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_cb('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('publishBLockTx %s to address_b58 %s', rv['tx_hash'], shared_addr)
|
||||||
tx_hash = bytes.fromhex(rv['tx_hash'])
|
tx_hash = bytes.fromhex(rv['tx_hash'])
|
||||||
|
|
||||||
if self._sc.debug:
|
if self._sc.debug:
|
||||||
i = 0
|
i = 0
|
||||||
while not self._sc.delay_event.is_set():
|
while not self._sc.delay_event.is_set():
|
||||||
params = {'out': True, 'pending': True, 'failed': True, 'pool': True, }
|
gt_params = {'out': True, 'pending': True, 'failed': True, 'pool': True, }
|
||||||
rv = self.rpc_wallet_cb('get_transfers', params)
|
rv = self.rpc_wallet('get_transfers', gt_params)
|
||||||
self._log.debug('get_transfers {}'.format(dumpj(rv)))
|
self._log.debug('get_transfers {}'.format(dumpj(rv)))
|
||||||
if 'pending' not in rv:
|
if 'pending' not in rv:
|
||||||
break
|
break
|
||||||
@@ -260,11 +345,6 @@ class XMRInterface(CoinInterface):
|
|||||||
Kbv = self.getPubkey(kbv)
|
Kbv = self.getPubkey(kbv)
|
||||||
address_b58 = xmr_util.encode_address(Kbv, Kbs)
|
address_b58 = xmr_util.encode_address(Kbv, Kbs)
|
||||||
|
|
||||||
try:
|
|
||||||
self.rpc_wallet_cb('close_wallet')
|
|
||||||
except Exception as e:
|
|
||||||
self._log.warning('close_wallet failed %s', str(e))
|
|
||||||
|
|
||||||
kbv_le = kbv[::-1]
|
kbv_le = kbv[::-1]
|
||||||
params = {
|
params = {
|
||||||
'restore_height': restore_height,
|
'restore_height': restore_height,
|
||||||
@@ -274,111 +354,57 @@ class XMRInterface(CoinInterface):
|
|||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
rv = self.rpc_wallet_cb('open_wallet', {'filename': address_b58})
|
self.openWallet(address_b58)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
rv = self.rpc_wallet_cb('generate_from_keys', params)
|
self.createWallet(params)
|
||||||
self._log.info('generate_from_keys %s', dumpj(rv))
|
self.openWallet(address_b58)
|
||||||
rv = self.rpc_wallet_cb('open_wallet', {'filename': address_b58})
|
|
||||||
|
|
||||||
self.rpc_wallet_cb('refresh', timeout=600)
|
self.rpc_wallet('refresh', timeout=self._walletrpctimeoutlong)
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# Debug
|
# Debug
|
||||||
try:
|
try:
|
||||||
current_height = self.rpc_wallet_cb('get_height')['height']
|
current_height = self.rpc_wallet('get_height')['height']
|
||||||
self._log.info('findTxB XMR current_height %d\nAddress: %s', current_height, address_b58)
|
self._log.info('findTxB XMR current_height %d\nAddress: %s', current_height, address_b58)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._log.info('rpc_cb 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'}
|
||||||
rv = self.rpc_wallet_cb('incoming_transfers', params)
|
transfers = self.rpc_wallet('incoming_transfers', params)
|
||||||
if 'transfers' in rv:
|
rv = None
|
||||||
for transfer in rv['transfers']:
|
if 'transfers' in transfers:
|
||||||
|
for transfer in transfers['transfers']:
|
||||||
|
# unlocked <- wallet->is_transfer_unlocked() checks unlock_time and CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE
|
||||||
|
if not transfer['unlocked']:
|
||||||
|
full_tx = self.rpc_wallet('get_transfer_by_txid', {'txid': transfer['tx_hash']})
|
||||||
|
unlock_time = full_tx['transfer']['unlock_time']
|
||||||
|
if unlock_time != 0:
|
||||||
|
self._log.warning('Coin b lock txn is locked: {}, unlock_time {}'.format(transfer['tx_hash'], unlock_time))
|
||||||
|
rv = -1
|
||||||
|
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']))
|
||||||
return -1
|
rv = -1
|
||||||
return None
|
return rv
|
||||||
|
|
||||||
def waitForLockTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height):
|
|
||||||
with self._mx_wallet:
|
|
||||||
Kbv_enc = self.encodePubkey(self.pubkey(kbv))
|
|
||||||
address_b58 = xmr_util.encode_address(Kbv_enc, self.encodePubkey(Kbs))
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.rpc_wallet_cb('close_wallet')
|
|
||||||
except Exception as e:
|
|
||||||
self._log.warning('close_wallet failed %s', str(e))
|
|
||||||
|
|
||||||
params = {
|
|
||||||
'filename': address_b58,
|
|
||||||
'address': address_b58,
|
|
||||||
'viewkey': b2h(kbv[::-1]),
|
|
||||||
'restore_height': restore_height,
|
|
||||||
}
|
|
||||||
self.rpc_wallet_cb('generate_from_keys', params)
|
|
||||||
|
|
||||||
self.rpc_wallet_cb('open_wallet', {'filename': address_b58})
|
|
||||||
# For a while after opening the wallet rpc cmds return empty data
|
|
||||||
|
|
||||||
num_tries = 40
|
|
||||||
for i in range(num_tries + 1):
|
|
||||||
try:
|
|
||||||
current_height = self.rpc_cb2('get_height')['height']
|
|
||||||
print('current_height', current_height)
|
|
||||||
except Exception as e:
|
|
||||||
self._log.warning('rpc_cb failed %s', str(e))
|
|
||||||
current_height = None # If the transfer is available it will be deep enough
|
|
||||||
|
|
||||||
# TODO: Make accepting current_height == None a user selectable option
|
|
||||||
# Or look for all transfers and check height
|
|
||||||
|
|
||||||
params = {'transfer_type': 'available'}
|
|
||||||
rv = self.rpc_wallet_cb('incoming_transfers', params)
|
|
||||||
print('rv', rv)
|
|
||||||
|
|
||||||
if 'transfers' in rv:
|
|
||||||
for transfer in rv['transfers']:
|
|
||||||
if transfer['amount'] == cb_swap_value \
|
|
||||||
and (current_height is None or current_height - transfer['block_height'] > cb_block_confirmed):
|
|
||||||
return True
|
|
||||||
|
|
||||||
# TODO: Is it necessary to check the address?
|
|
||||||
'''
|
|
||||||
rv = self.rpc_wallet_cb('get_balance')
|
|
||||||
print('get_balance', rv)
|
|
||||||
|
|
||||||
if 'per_subaddress' in rv:
|
|
||||||
for sub_addr in rv['per_subaddress']:
|
|
||||||
if sub_addr['address'] == address_b58:
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
if i >= num_tries:
|
|
||||||
raise ValueError('Balance not confirming on node')
|
|
||||||
self._sc.delay_event.wait(1.0)
|
|
||||||
if self._sc.delay_event.is_set():
|
|
||||||
raise ValueError('Stopped')
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def findTxnByHash(self, txid):
|
def findTxnByHash(self, txid):
|
||||||
with self._mx_wallet:
|
with self._mx_wallet:
|
||||||
self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename})
|
self.openWallet(self._wallet_filename)
|
||||||
self.rpc_wallet_cb('refresh')
|
self.rpc_wallet('refresh', timeout=self._walletrpctimeoutlong)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
current_height = self.rpc_cb2('get_height', timeout=30)['height']
|
current_height = self.rpc2('get_height', timeout=self._rpctimeout)['height']
|
||||||
self._log.info('findTxnByHash XMR current_height %d\nhash: %s', current_height, txid)
|
self._log.info('findTxnByHash XMR current_height %d\nhash: %s', current_height, txid)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._log.info('rpc_cb 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_cb('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 \
|
||||||
@@ -387,17 +413,16 @@ class XMRInterface(CoinInterface):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def spendBLockTx(self, chain_b_lock_txid, address_to, kbv, kbs, cb_swap_value, b_fee_rate, restore_height, spend_actual_balance=False):
|
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) -> bytes:
|
||||||
|
'''
|
||||||
|
Notes:
|
||||||
|
"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)
|
address_b58 = xmr_util.encode_address(Kbv, Kbs)
|
||||||
|
|
||||||
try:
|
|
||||||
self.rpc_wallet_cb('close_wallet')
|
|
||||||
except Exception as e:
|
|
||||||
self._log.warning('close_wallet failed %s', str(e))
|
|
||||||
|
|
||||||
wallet_filename = address_b58 + '_spend'
|
wallet_filename = address_b58 + '_spend'
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
@@ -409,18 +434,16 @@ class XMRInterface(CoinInterface):
|
|||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.rpc_wallet_cb('open_wallet', {'filename': wallet_filename})
|
self.openWallet(wallet_filename)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
rv = self.rpc_wallet_cb('generate_from_keys', params)
|
self.createWallet(params)
|
||||||
self._log.info('generate_from_keys %s', dumpj(rv))
|
self.openWallet(wallet_filename)
|
||||||
self.rpc_wallet_cb('open_wallet', {'filename': wallet_filename})
|
|
||||||
|
|
||||||
self.rpc_wallet_cb('refresh')
|
|
||||||
rv = self.rpc_wallet_cb('get_balance')
|
|
||||||
|
|
||||||
|
self.rpc_wallet('refresh')
|
||||||
|
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_cb('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:
|
||||||
@@ -445,57 +468,106 @@ class XMRInterface(CoinInterface):
|
|||||||
if self._fee_priority > 0:
|
if self._fee_priority > 0:
|
||||||
params['priority'] = self._fee_priority
|
params['priority'] = self._fee_priority
|
||||||
|
|
||||||
rv = self.rpc_wallet_cb('sweep_all', params)
|
rv = self.rpc_wallet('sweep_all', params)
|
||||||
self._log.debug('sweep_all {}'.format(json.dumps(rv)))
|
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, subfee):
|
def withdrawCoin(self, value: int, addr_to: str, subfee: bool) -> str:
|
||||||
with self._mx_wallet:
|
with self._mx_wallet:
|
||||||
value_sats = make_int(value, self.exp())
|
value_sats = make_int(value, self.exp())
|
||||||
|
|
||||||
self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename})
|
self.openWallet(self._wallet_filename)
|
||||||
|
self.rpc_wallet('refresh')
|
||||||
|
|
||||||
if subfee:
|
if subfee:
|
||||||
balance = self.rpc_wallet_cb('get_balance')
|
balance = self.rpc_wallet('get_balance')
|
||||||
if balance['unlocked_balance'] - value_sats <= 10:
|
diff = balance['unlocked_balance'] - value_sats
|
||||||
|
if diff >= 0 and diff <= 10:
|
||||||
self._log.info('subfee enabled and value close to total, using sweep_all.')
|
self._log.info('subfee enabled and value close to total, using sweep_all.')
|
||||||
params = {'address': addr_to}
|
params = {'address': addr_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_cb('sweep_all', params)
|
rv = self.rpc_wallet('sweep_all', params)
|
||||||
return rv['tx_hash_list'][0]
|
return rv['tx_hash_list'][0]
|
||||||
raise ValueError('Withdraw value must be close to total to use subfee/sweep_all.')
|
raise ValueError('Withdraw value must be close to total to use subfee/sweep_all.')
|
||||||
|
|
||||||
params = {'destinations': [{'amount': value_sats, 'address': addr_to}]}
|
params = {'destinations': [{'amount': value_sats, 'address': addr_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_cb('transfer', params)
|
rv = self.rpc_wallet('transfer', params)
|
||||||
return rv['tx_hash']
|
return rv['tx_hash']
|
||||||
|
|
||||||
def showLockTransfers(self, Kbv, Kbs):
|
def showLockTransfers(self, kbv, Kbs, restore_height):
|
||||||
with self._mx_wallet:
|
with self._mx_wallet:
|
||||||
try:
|
try:
|
||||||
|
Kbv = self.getPubkey(kbv)
|
||||||
address_b58 = xmr_util.encode_address(Kbv, Kbs)
|
address_b58 = xmr_util.encode_address(Kbv, Kbs)
|
||||||
wallet_file = address_b58 + '_spend'
|
wallet_file = address_b58 + '_spend'
|
||||||
try:
|
try:
|
||||||
self.rpc_wallet_cb('open_wallet', {'filename': wallet_file})
|
self.openWallet(wallet_file)
|
||||||
except Exception:
|
except Exception:
|
||||||
wallet_file = address_b58
|
wallet_file = address_b58
|
||||||
self.rpc_wallet_cb('open_wallet', {'filename': wallet_file})
|
try:
|
||||||
|
self.openWallet(wallet_file)
|
||||||
|
except Exception:
|
||||||
|
self._log.info(f'showLockTransfers trying to create wallet for address {address_b58}.')
|
||||||
|
kbv_le = kbv[::-1]
|
||||||
|
params = {
|
||||||
|
'restore_height': restore_height,
|
||||||
|
'filename': address_b58,
|
||||||
|
'address': address_b58,
|
||||||
|
'viewkey': b2h(kbv_le),
|
||||||
|
}
|
||||||
|
self.createWallet(params)
|
||||||
|
self.openWallet(address_b58)
|
||||||
|
|
||||||
self.rpc_wallet_cb('refresh')
|
self.rpc_wallet('refresh')
|
||||||
|
|
||||||
rv = self.rpc_wallet_cb('get_transfers', {'in': True, 'out': True, 'pending': True, 'failed': True})
|
rv = self.rpc_wallet('get_transfers', {'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):
|
def getSpendableBalance(self) -> int:
|
||||||
with self._mx_wallet:
|
with self._mx_wallet:
|
||||||
self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename})
|
self.openWallet(self._wallet_filename)
|
||||||
|
|
||||||
self.rpc_wallet_cb('refresh')
|
self.rpc_wallet('refresh')
|
||||||
balance_info = self.rpc_wallet_cb('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):
|
||||||
|
self._log.info('changeWalletPassword - {}'.format(self.ticker()))
|
||||||
|
orig_password = self._wallet_password
|
||||||
|
if old_password != '':
|
||||||
|
self._wallet_password = old_password
|
||||||
|
try:
|
||||||
|
self.openWallet(self._wallet_filename)
|
||||||
|
self.rpc_wallet('change_wallet_password', {'old_password': old_password, 'new_password': new_password})
|
||||||
|
except Exception as e:
|
||||||
|
self._wallet_password = orig_password
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def unlockWallet(self, password: str) -> None:
|
||||||
|
self._log.info('unlockWallet - {}'.format(self.ticker()))
|
||||||
|
self._wallet_password = password
|
||||||
|
|
||||||
|
if not self._have_checked_seed:
|
||||||
|
self._sc.checkWalletSeed(self.coin_type())
|
||||||
|
|
||||||
|
def lockWallet(self) -> None:
|
||||||
|
self._log.info('lockWallet - {}'.format(self.ticker()))
|
||||||
|
self._wallet_password = None
|
||||||
|
|
||||||
|
def isAddressMine(self, address):
|
||||||
|
# TODO
|
||||||
|
return True
|
||||||
|
|
||||||
|
def ensureFunds(self, amount: int) -> None:
|
||||||
|
if self.getSpendableBalance() < amount:
|
||||||
|
raise ValueError('Balance too low')
|
||||||
|
|
||||||
|
def getTransaction(self, txid: bytes):
|
||||||
|
return self.rpc2('get_transactions', {'txs_hashes': [txid.hex(), ]})
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2020-2022 tecnovert
|
# Copyright (c) 2020-2024 tecnovert
|
||||||
# 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 random
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
from .util import (
|
from .util import (
|
||||||
|
ensure,
|
||||||
toBool,
|
toBool,
|
||||||
)
|
)
|
||||||
from .basicswap_util import (
|
from .basicswap_util import (
|
||||||
strBidState,
|
strBidState,
|
||||||
SwapTypes,
|
SwapTypes,
|
||||||
|
NotificationTypes as NT,
|
||||||
)
|
)
|
||||||
from .chainparams import (
|
from .chainparams import (
|
||||||
Coins,
|
Coins,
|
||||||
@@ -20,6 +23,7 @@ from .chainparams import (
|
|||||||
)
|
)
|
||||||
from .ui.util import (
|
from .ui.util import (
|
||||||
PAGE_LIMIT,
|
PAGE_LIMIT,
|
||||||
|
getCoinName,
|
||||||
getCoinType,
|
getCoinType,
|
||||||
inputAmount,
|
inputAmount,
|
||||||
describeBid,
|
describeBid,
|
||||||
@@ -29,22 +33,25 @@ from .ui.util import (
|
|||||||
have_data_entry,
|
have_data_entry,
|
||||||
tickerToCoinId,
|
tickerToCoinId,
|
||||||
listOldBidStates,
|
listOldBidStates,
|
||||||
|
checkAddressesOwned,
|
||||||
)
|
)
|
||||||
from .ui.page_offers import postNewOffer
|
from .ui.page_offers import postNewOffer
|
||||||
from .protocols.xmr_swap_1 import recoverNoScriptTxnWithKey, getChainBSplitKey
|
from .protocols.xmr_swap_1 import recoverNoScriptTxnWithKey, getChainBSplitKey
|
||||||
|
|
||||||
|
|
||||||
def js_error(self, error_str):
|
def getFormData(post_string: str, is_json: bool):
|
||||||
error_str_json = json.dumps({'error': error_str})
|
if post_string == '':
|
||||||
return bytes(error_str_json, 'UTF-8')
|
raise ValueError('No post data')
|
||||||
|
if is_json:
|
||||||
|
form_data = json.loads(post_string)
|
||||||
|
form_data['is_json'] = True
|
||||||
|
else:
|
||||||
|
form_data = urllib.parse.parse_qs(post_string)
|
||||||
|
return form_data
|
||||||
|
|
||||||
|
|
||||||
def withdraw_coin(swap_client, coin_type, post_string, is_json):
|
def withdraw_coin(swap_client, coin_type, post_string, is_json):
|
||||||
if is_json:
|
post_data = getFormData(post_string, is_json)
|
||||||
post_data = json.loads(post_string)
|
|
||||||
post_data['is_json'] = True
|
|
||||||
else:
|
|
||||||
post_data = urllib.parse.parse_qs(post_string)
|
|
||||||
|
|
||||||
value = get_data_entry(post_data, 'value')
|
value = get_data_entry(post_data, 'value')
|
||||||
address = get_data_entry(post_data, 'address')
|
address = get_data_entry(post_data, 'address')
|
||||||
@@ -56,28 +63,43 @@ def withdraw_coin(swap_client, coin_type, post_string, is_json):
|
|||||||
type_from = get_data_entry_or(post_data, 'type_from', 'plain')
|
type_from = get_data_entry_or(post_data, 'type_from', 'plain')
|
||||||
type_to = get_data_entry_or(post_data, 'type_to', 'plain')
|
type_to = get_data_entry_or(post_data, 'type_to', 'plain')
|
||||||
txid_hex = swap_client.withdrawParticl(type_from, type_to, value, address, subfee)
|
txid_hex = swap_client.withdrawParticl(type_from, type_to, value, address, subfee)
|
||||||
|
elif coin_type == Coins.LTC:
|
||||||
|
type_from = get_data_entry_or(post_data, 'type_from', 'plain')
|
||||||
|
txid_hex = swap_client.withdrawLTC(type_from, value, address, subfee)
|
||||||
else:
|
else:
|
||||||
txid_hex = swap_client.withdrawCoin(coin_type, value, address, subfee)
|
txid_hex = swap_client.withdrawCoin(coin_type, value, address, subfee)
|
||||||
|
|
||||||
return {'txid': txid_hex}
|
return {'txid': txid_hex}
|
||||||
|
|
||||||
|
|
||||||
def js_coins(self, url_split, post_string, is_json):
|
def js_error(self, error_str) -> bytes:
|
||||||
|
error_str_json = json.dumps({'error': error_str})
|
||||||
|
return bytes(error_str_json, 'UTF-8')
|
||||||
|
|
||||||
|
|
||||||
|
def js_coins(self, url_split, post_string, is_json) -> bytes:
|
||||||
swap_client = self.server.swap_client
|
swap_client = self.server.swap_client
|
||||||
|
|
||||||
coins = []
|
coins = []
|
||||||
for coin in Coins:
|
for coin in Coins:
|
||||||
cc = swap_client.coin_clients[coin]
|
cc = swap_client.coin_clients[coin]
|
||||||
|
coin_chainparams = chainparams[cc['coin']]
|
||||||
|
coin_active: bool = False if cc['connection_type'] == 'none' else True
|
||||||
|
if coin == Coins.LTC_MWEB:
|
||||||
|
coin_active = False
|
||||||
entry = {
|
entry = {
|
||||||
'id': int(coin),
|
'id': int(coin),
|
||||||
'ticker': chainparams[cc['coin']]['ticker'],
|
'ticker': coin_chainparams['ticker'],
|
||||||
'name': cc['name'].capitalize(),
|
'name': getCoinName(coin),
|
||||||
'active': False if cc['connection_type'] == 'none' else True,
|
'active': coin_active,
|
||||||
|
'decimal_places': coin_chainparams['decimal_places'],
|
||||||
}
|
}
|
||||||
if coin == Coins.PART_ANON:
|
if coin == Coins.PART_ANON:
|
||||||
entry['variant'] = 'Anon'
|
entry['variant'] = 'Anon'
|
||||||
elif coin == Coins.PART_BLIND:
|
elif coin == Coins.PART_BLIND:
|
||||||
entry['variant'] = 'Blind'
|
entry['variant'] = 'Blind'
|
||||||
|
elif coin == Coins.LTC_MWEB:
|
||||||
|
entry['variant'] = 'MWEB'
|
||||||
coins.append(entry)
|
coins.append(entry)
|
||||||
|
|
||||||
return bytes(json.dumps(coins), 'UTF-8')
|
return bytes(json.dumps(coins), 'UTF-8')
|
||||||
@@ -85,6 +107,7 @@ def js_coins(self, url_split, post_string, is_json):
|
|||||||
|
|
||||||
def js_wallets(self, url_split, post_string, is_json):
|
def js_wallets(self, url_split, post_string, is_json):
|
||||||
swap_client = self.server.swap_client
|
swap_client = self.server.swap_client
|
||||||
|
swap_client.checkSystemStatus()
|
||||||
if len(url_split) > 3:
|
if len(url_split) > 3:
|
||||||
ticker_str = url_split[3]
|
ticker_str = url_split[3]
|
||||||
coin_type = tickerToCoinId(ticker_str)
|
coin_type = tickerToCoinId(ticker_str)
|
||||||
@@ -93,33 +116,52 @@ def js_wallets(self, url_split, post_string, is_json):
|
|||||||
cmd = url_split[4]
|
cmd = url_split[4]
|
||||||
if cmd == 'withdraw':
|
if cmd == 'withdraw':
|
||||||
return bytes(json.dumps(withdraw_coin(swap_client, coin_type, post_string, is_json)), 'UTF-8')
|
return bytes(json.dumps(withdraw_coin(swap_client, coin_type, post_string, is_json)), 'UTF-8')
|
||||||
if cmd == 'nextdepositaddr':
|
elif cmd == 'nextdepositaddr':
|
||||||
return bytes(json.dumps(swap_client.cacheNewAddressForCoin(coin_type)), 'UTF-8')
|
return bytes(json.dumps(swap_client.cacheNewAddressForCoin(coin_type)), 'UTF-8')
|
||||||
|
elif cmd == 'createutxo':
|
||||||
|
post_data = getFormData(post_string, is_json)
|
||||||
|
ci = swap_client.ci(coin_type)
|
||||||
|
value = ci.make_int(get_data_entry(post_data, 'value'))
|
||||||
|
txid_hex, new_addr = ci.createUTXO(value)
|
||||||
|
return bytes(json.dumps({'txid': txid_hex, 'address': new_addr}), 'UTF-8')
|
||||||
|
elif cmd == 'reseed':
|
||||||
|
swap_client.reseedWallet(coin_type)
|
||||||
|
return bytes(json.dumps({'reseeded': True}), 'UTF-8')
|
||||||
|
elif cmd == 'newstealthaddress':
|
||||||
|
if coin_type != Coins.PART:
|
||||||
|
raise ValueError('Invalid coin for command')
|
||||||
|
return bytes(json.dumps(swap_client.ci(coin_type).getNewStealthAddress()), 'UTF-8')
|
||||||
|
elif cmd == 'newmwebaddress':
|
||||||
|
if coin_type not in (Coins.LTC, Coins.LTC_MWEB):
|
||||||
|
raise ValueError('Invalid coin for command')
|
||||||
|
return bytes(json.dumps(swap_client.ci(coin_type).getNewMwebAddress()), 'UTF-8')
|
||||||
|
|
||||||
raise ValueError('Unknown command')
|
raise ValueError('Unknown command')
|
||||||
|
|
||||||
|
if coin_type == Coins.LTC_MWEB:
|
||||||
|
coin_type = Coins.LTC
|
||||||
rv = swap_client.getWalletInfo(coin_type)
|
rv = swap_client.getWalletInfo(coin_type)
|
||||||
rv.update(swap_client.getBlockchainInfo(coin_type))
|
rv.update(swap_client.getBlockchainInfo(coin_type))
|
||||||
|
ci = swap_client.ci(coin_type)
|
||||||
|
checkAddressesOwned(swap_client, ci, rv)
|
||||||
return bytes(json.dumps(rv), 'UTF-8')
|
return bytes(json.dumps(rv), 'UTF-8')
|
||||||
return bytes(json.dumps(self.server.swap_client.getWalletsInfo({'ticker_key': True})), 'UTF-8')
|
|
||||||
|
return bytes(json.dumps(swap_client.getWalletsInfo({'ticker_key': True})), 'UTF-8')
|
||||||
|
|
||||||
|
|
||||||
def js_offers(self, url_split, post_string, is_json, sent=False):
|
def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
|
||||||
swap_client = self.server.swap_client
|
swap_client = self.server.swap_client
|
||||||
|
swap_client.checkSystemStatus()
|
||||||
offer_id = None
|
offer_id = None
|
||||||
if len(url_split) > 3:
|
if len(url_split) > 3:
|
||||||
if url_split[3] == 'new':
|
if url_split[3] == 'new':
|
||||||
if post_string == '':
|
form_data = getFormData(post_string, is_json)
|
||||||
raise ValueError('No post data')
|
|
||||||
if is_json:
|
|
||||||
form_data = json.loads(post_string)
|
|
||||||
form_data['is_json'] = True
|
|
||||||
else:
|
|
||||||
form_data = urllib.parse.parse_qs(post_string)
|
|
||||||
offer_id = postNewOffer(swap_client, form_data)
|
offer_id = postNewOffer(swap_client, form_data)
|
||||||
rv = {'offer_id': offer_id.hex()}
|
rv = {'offer_id': offer_id.hex()}
|
||||||
return bytes(json.dumps(rv), 'UTF-8')
|
return bytes(json.dumps(rv), 'UTF-8')
|
||||||
offer_id = bytes.fromhex(url_split[3])
|
offer_id = bytes.fromhex(url_split[3])
|
||||||
|
|
||||||
|
with_extra_info = False
|
||||||
filters = {
|
filters = {
|
||||||
'coin_from': -1,
|
'coin_from': -1,
|
||||||
'coin_to': -1,
|
'coin_to': -1,
|
||||||
@@ -133,11 +175,7 @@ def js_offers(self, url_split, post_string, is_json, sent=False):
|
|||||||
filters['offer_id'] = offer_id
|
filters['offer_id'] = offer_id
|
||||||
|
|
||||||
if post_string != '':
|
if post_string != '':
|
||||||
if is_json:
|
post_data = getFormData(post_string, is_json)
|
||||||
post_data = json.loads(post_string)
|
|
||||||
post_data['is_json'] = True
|
|
||||||
else:
|
|
||||||
post_data = urllib.parse.parse_qs(post_string)
|
|
||||||
filters['coin_from'] = setCoinFilter(post_data, 'coin_from')
|
filters['coin_from'] = setCoinFilter(post_data, 'coin_from')
|
||||||
filters['coin_to'] = setCoinFilter(post_data, 'coin_to')
|
filters['coin_to'] = setCoinFilter(post_data, 'coin_to')
|
||||||
|
|
||||||
@@ -150,18 +188,26 @@ def js_offers(self, url_split, post_string, is_json, sent=False):
|
|||||||
assert (sort_dir in ['asc', 'desc']), 'Invalid sort dir'
|
assert (sort_dir in ['asc', 'desc']), 'Invalid sort dir'
|
||||||
filters['sort_dir'] = sort_dir
|
filters['sort_dir'] = sort_dir
|
||||||
|
|
||||||
if b'offset' in post_data:
|
if have_data_entry(post_data, 'offset'):
|
||||||
filters['offset'] = int(get_data_entry(post_data, 'offset'))
|
filters['offset'] = int(get_data_entry(post_data, 'offset'))
|
||||||
if b'limit' in post_data:
|
if have_data_entry(post_data, 'limit'):
|
||||||
filters['limit'] = int(get_data_entry(post_data, 'limit'))
|
filters['limit'] = int(get_data_entry(post_data, 'limit'))
|
||||||
assert (filters['limit'] > 0 and filters['limit'] <= PAGE_LIMIT), 'Invalid limit'
|
assert (filters['limit'] > 0 and filters['limit'] <= PAGE_LIMIT), 'Invalid limit'
|
||||||
|
if have_data_entry(post_data, 'active'):
|
||||||
|
filters['active'] = get_data_entry(post_data, 'active')
|
||||||
|
if have_data_entry(post_data, 'include_sent'):
|
||||||
|
filters['include_sent'] = toBool(get_data_entry(post_data, 'include_sent'))
|
||||||
|
|
||||||
offers = self.server.swap_client.listOffers(sent, filters)
|
if have_data_entry(post_data, 'with_extra_info'):
|
||||||
|
with_extra_info = toBool(get_data_entry(post_data, 'with_extra_info'))
|
||||||
|
|
||||||
|
offers = swap_client.listOffers(sent, filters)
|
||||||
rv = []
|
rv = []
|
||||||
for o in offers:
|
for o in offers:
|
||||||
ci_from = self.server.swap_client.ci(o.coin_from)
|
ci_from = swap_client.ci(o.coin_from)
|
||||||
ci_to = self.server.swap_client.ci(o.coin_to)
|
ci_to = swap_client.ci(o.coin_to)
|
||||||
rv.append({
|
offer_data = {
|
||||||
|
'swap_type': o.swap_type,
|
||||||
'addr_from': o.addr_from,
|
'addr_from': o.addr_from,
|
||||||
'addr_to': o.addr_to,
|
'addr_to': o.addr_to,
|
||||||
'offer_id': o.offer_id.hex(),
|
'offer_id': o.offer_id.hex(),
|
||||||
@@ -172,26 +218,90 @@ def js_offers(self, url_split, post_string, is_json, sent=False):
|
|||||||
'amount_from': ci_from.format_amount(o.amount_from),
|
'amount_from': ci_from.format_amount(o.amount_from),
|
||||||
'amount_to': ci_to.format_amount((o.amount_from * o.rate) // ci_from.COIN()),
|
'amount_to': ci_to.format_amount((o.amount_from * o.rate) // ci_from.COIN()),
|
||||||
'rate': ci_to.format_amount(o.rate),
|
'rate': ci_to.format_amount(o.rate),
|
||||||
})
|
}
|
||||||
|
if with_extra_info:
|
||||||
|
offer_data['amount_negotiable'] = o.amount_negotiable
|
||||||
|
offer_data['rate_negotiable'] = o.rate_negotiable
|
||||||
|
|
||||||
|
if o.swap_type == SwapTypes.XMR_SWAP:
|
||||||
|
_, xmr_offer = swap_client.getXmrOffer(o.offer_id)
|
||||||
|
offer_data['lock_time_1'] = xmr_offer.lock_time_1
|
||||||
|
offer_data['lock_time_2'] = xmr_offer.lock_time_2
|
||||||
|
|
||||||
|
offer_data['feerate_from'] = xmr_offer.a_fee_rate
|
||||||
|
offer_data['feerate_to'] = xmr_offer.b_fee_rate
|
||||||
|
else:
|
||||||
|
offer_data['feerate_from'] = o.from_feerate
|
||||||
|
offer_data['feerate_to'] = o.to_feerate
|
||||||
|
|
||||||
|
rv.append(offer_data)
|
||||||
return bytes(json.dumps(rv), 'UTF-8')
|
return bytes(json.dumps(rv), 'UTF-8')
|
||||||
|
|
||||||
|
|
||||||
def js_sentoffers(self, url_split, post_string, is_json):
|
def js_sentoffers(self, url_split, post_string, is_json) -> bytes:
|
||||||
return self.js_offers(url_split, post_string, is_json, True)
|
return js_offers(self, url_split, post_string, is_json, True)
|
||||||
|
|
||||||
|
|
||||||
def js_bids(self, url_split, post_string, is_json):
|
def parseBidFilters(post_data):
|
||||||
|
offer_id = None
|
||||||
|
filters = {}
|
||||||
|
|
||||||
|
if have_data_entry(post_data, 'offer_id'):
|
||||||
|
offer_id = bytes.fromhex(get_data_entry(post_data, 'offer_id'))
|
||||||
|
assert (len(offer_id) == 28)
|
||||||
|
|
||||||
|
if have_data_entry(post_data, 'sort_by'):
|
||||||
|
sort_by = get_data_entry(post_data, 'sort_by')
|
||||||
|
assert (sort_by in ['created_at', ]), 'Invalid sort by'
|
||||||
|
filters['sort_by'] = sort_by
|
||||||
|
if have_data_entry(post_data, 'sort_dir'):
|
||||||
|
sort_dir = get_data_entry(post_data, 'sort_dir')
|
||||||
|
assert (sort_dir in ['asc', 'desc']), 'Invalid sort dir'
|
||||||
|
filters['sort_dir'] = sort_dir
|
||||||
|
|
||||||
|
if have_data_entry(post_data, 'offset'):
|
||||||
|
filters['offset'] = int(get_data_entry(post_data, 'offset'))
|
||||||
|
if have_data_entry(post_data, 'limit'):
|
||||||
|
filters['limit'] = int(get_data_entry(post_data, 'limit'))
|
||||||
|
assert (filters['limit'] > 0 and filters['limit'] <= PAGE_LIMIT), 'Invalid limit'
|
||||||
|
|
||||||
|
if have_data_entry(post_data, 'with_available_or_active'):
|
||||||
|
filters['with_available_or_active'] = toBool(get_data_entry(post_data, 'with_available_or_active'))
|
||||||
|
elif have_data_entry(post_data, 'with_expired'):
|
||||||
|
filters['with_expired'] = toBool(get_data_entry(post_data, 'with_expired'))
|
||||||
|
|
||||||
|
if have_data_entry(post_data, 'with_extra_info'):
|
||||||
|
filters['with_extra_info'] = toBool(get_data_entry(post_data, 'with_extra_info'))
|
||||||
|
|
||||||
|
return offer_id, filters
|
||||||
|
|
||||||
|
|
||||||
|
def formatBids(swap_client, bids, filters) -> bytes:
|
||||||
|
with_extra_info = filters.get('with_extra_info', False)
|
||||||
|
rv = []
|
||||||
|
for b in bids:
|
||||||
|
bid_data = {
|
||||||
|
'bid_id': b[2].hex(),
|
||||||
|
'offer_id': b[3].hex(),
|
||||||
|
'created_at': b[0],
|
||||||
|
'expire_at': b[1],
|
||||||
|
'coin_from': b[9],
|
||||||
|
'amount_from': swap_client.ci(b[9]).format_amount(b[4]),
|
||||||
|
'bid_rate': swap_client.ci(b[14]).format_amount(b[10]),
|
||||||
|
'bid_state': strBidState(b[5])
|
||||||
|
}
|
||||||
|
if with_extra_info:
|
||||||
|
bid_data['addr_from'] = b[11]
|
||||||
|
rv.append(bid_data)
|
||||||
|
return bytes(json.dumps(rv), 'UTF-8')
|
||||||
|
|
||||||
|
|
||||||
|
def js_bids(self, url_split, post_string: str, is_json: bool) -> bytes:
|
||||||
swap_client = self.server.swap_client
|
swap_client = self.server.swap_client
|
||||||
|
swap_client.checkSystemStatus()
|
||||||
if len(url_split) > 3:
|
if len(url_split) > 3:
|
||||||
if url_split[3] == 'new':
|
if url_split[3] == 'new':
|
||||||
if post_string == '':
|
post_data = getFormData(post_string, is_json)
|
||||||
raise ValueError('No post data')
|
|
||||||
if is_json:
|
|
||||||
post_data = json.loads(post_string)
|
|
||||||
post_data['is_json'] = True
|
|
||||||
else:
|
|
||||||
post_data = urllib.parse.parse_qs(post_string)
|
|
||||||
|
|
||||||
offer_id = bytes.fromhex(get_data_entry(post_data, 'offer_id'))
|
offer_id = bytes.fromhex(get_data_entry(post_data, 'offer_id'))
|
||||||
assert (len(offer_id) == 28)
|
assert (len(offer_id) == 28)
|
||||||
@@ -238,17 +348,19 @@ def js_bids(self, url_split, post_string, is_json):
|
|||||||
bid_id = bytes.fromhex(url_split[3])
|
bid_id = bytes.fromhex(url_split[3])
|
||||||
assert (len(bid_id) == 28)
|
assert (len(bid_id) == 28)
|
||||||
|
|
||||||
|
show_txns = False
|
||||||
if post_string != '':
|
if post_string != '':
|
||||||
if is_json:
|
post_data = getFormData(post_string, is_json)
|
||||||
post_data = json.loads(post_string)
|
|
||||||
post_data['is_json'] = True
|
|
||||||
else:
|
|
||||||
post_data = urllib.parse.parse_qs(post_string)
|
|
||||||
if have_data_entry(post_data, 'accept'):
|
if have_data_entry(post_data, 'accept'):
|
||||||
swap_client.acceptBid(bid_id)
|
swap_client.acceptBid(bid_id)
|
||||||
|
elif have_data_entry(post_data, 'abandon'):
|
||||||
|
swap_client.abandonBid(bid_id)
|
||||||
elif have_data_entry(post_data, 'debugind'):
|
elif have_data_entry(post_data, 'debugind'):
|
||||||
swap_client.setBidDebugInd(bid_id, int(get_data_entry(post_data, 'debugind')))
|
swap_client.setBidDebugInd(bid_id, int(get_data_entry(post_data, 'debugind')))
|
||||||
|
|
||||||
|
if have_data_entry(post_data, 'show_extra'):
|
||||||
|
show_txns = True
|
||||||
|
|
||||||
bid, xmr_swap, offer, xmr_offer, events = swap_client.getXmrBidAndOffer(bid_id)
|
bid, xmr_swap, offer, xmr_offer, events = swap_client.getXmrBidAndOffer(bid_id)
|
||||||
assert (bid), 'Unknown bid ID'
|
assert (bid), 'Unknown bid ID'
|
||||||
|
|
||||||
@@ -264,47 +376,46 @@ def js_bids(self, url_split, post_string, is_json):
|
|||||||
return bytes(json.dumps(old_states), 'UTF-8')
|
return bytes(json.dumps(old_states), 'UTF-8')
|
||||||
|
|
||||||
edit_bid = False
|
edit_bid = False
|
||||||
show_txns = False
|
|
||||||
data = describeBid(swap_client, bid, xmr_swap, offer, xmr_offer, events, edit_bid, show_txns, for_api=True)
|
data = describeBid(swap_client, bid, xmr_swap, offer, xmr_offer, events, edit_bid, show_txns, for_api=True)
|
||||||
return bytes(json.dumps(data), 'UTF-8')
|
return bytes(json.dumps(data), 'UTF-8')
|
||||||
|
|
||||||
bids = swap_client.listBids()
|
post_data = {} if post_string == '' else getFormData(post_string, is_json)
|
||||||
return bytes(json.dumps([{
|
offer_id, filters = parseBidFilters(post_data)
|
||||||
'bid_id': b[2].hex(),
|
|
||||||
'offer_id': b[3].hex(),
|
bids = swap_client.listBids(offer_id=offer_id, filters=filters)
|
||||||
'created_at': b[0],
|
return formatBids(swap_client, bids, filters)
|
||||||
'expire_at': b[1],
|
|
||||||
'coin_from': b[9],
|
|
||||||
'amount_from': swap_client.ci(b[9]).format_amount(b[4]),
|
|
||||||
'bid_state': strBidState(b[5])
|
|
||||||
} for b in bids]), 'UTF-8')
|
|
||||||
|
|
||||||
|
|
||||||
def js_sentbids(self, url_split, post_string, is_json):
|
def js_sentbids(self, url_split, post_string, is_json) -> bytes:
|
||||||
return bytes(json.dumps(self.server.swap_client.listBids(sent=True)), 'UTF-8')
|
swap_client = self.server.swap_client
|
||||||
|
swap_client.checkSystemStatus()
|
||||||
|
post_data = getFormData(post_string, is_json)
|
||||||
|
offer_id, filters = parseBidFilters(post_data)
|
||||||
|
|
||||||
|
bids = swap_client.listBids(sent=True, offer_id=offer_id, filters=filters)
|
||||||
|
return formatBids(swap_client, bids, filters)
|
||||||
|
|
||||||
|
|
||||||
def js_network(self, url_split, post_string, is_json):
|
def js_network(self, url_split, post_string, is_json) -> bytes:
|
||||||
return bytes(json.dumps(self.server.swap_client.get_network_info()), 'UTF-8')
|
swap_client = self.server.swap_client
|
||||||
|
swap_client.checkSystemStatus()
|
||||||
|
return bytes(json.dumps(swap_client.get_network_info()), 'UTF-8')
|
||||||
|
|
||||||
|
|
||||||
def js_revokeoffer(self, url_split, post_string, is_json):
|
def js_revokeoffer(self, url_split, post_string, is_json) -> bytes:
|
||||||
|
swap_client = self.server.swap_client
|
||||||
|
swap_client.checkSystemStatus()
|
||||||
offer_id = bytes.fromhex(url_split[3])
|
offer_id = bytes.fromhex(url_split[3])
|
||||||
assert (len(offer_id) == 28)
|
assert (len(offer_id) == 28)
|
||||||
self.server.swap_client.revokeOffer(offer_id)
|
swap_client.revokeOffer(offer_id)
|
||||||
return bytes(json.dumps({'revoked_offer': offer_id.hex()}), 'UTF-8')
|
return bytes(json.dumps({'revoked_offer': offer_id.hex()}), 'UTF-8')
|
||||||
|
|
||||||
|
|
||||||
def js_smsgaddresses(self, url_split, post_string, is_json):
|
def js_smsgaddresses(self, url_split, post_string, is_json) -> bytes:
|
||||||
swap_client = self.server.swap_client
|
swap_client = self.server.swap_client
|
||||||
|
swap_client.checkSystemStatus()
|
||||||
|
post_data = {} if post_string == '' else getFormData(post_string, is_json)
|
||||||
if len(url_split) > 3:
|
if len(url_split) > 3:
|
||||||
if post_string == '':
|
|
||||||
raise ValueError('No post data')
|
|
||||||
if is_json:
|
|
||||||
post_data = json.loads(post_string)
|
|
||||||
post_data['is_json'] = True
|
|
||||||
else:
|
|
||||||
post_data = urllib.parse.parse_qs(post_string)
|
|
||||||
if url_split[3] == 'new':
|
if url_split[3] == 'new':
|
||||||
addressnote = get_data_entry_or(post_data, 'addressnote', '')
|
addressnote = get_data_entry_or(post_data, 'addressnote', '')
|
||||||
new_addr, pubkey = swap_client.newSMSGAddress(addressnote=addressnote)
|
new_addr, pubkey = swap_client.newSMSGAddress(addressnote=addressnote)
|
||||||
@@ -321,17 +432,15 @@ def js_smsgaddresses(self, url_split, post_string, is_json):
|
|||||||
new_addr = swap_client.editSMSGAddress(address, activeind, addressnote)
|
new_addr = swap_client.editSMSGAddress(address, activeind, addressnote)
|
||||||
return bytes(json.dumps({'edited_address': address}), 'UTF-8')
|
return bytes(json.dumps({'edited_address': address}), 'UTF-8')
|
||||||
|
|
||||||
return bytes(json.dumps(swap_client.listAllSMSGAddresses()), 'UTF-8')
|
filters = {
|
||||||
|
'exclude_inactive': post_data.get('exclude_inactive', True),
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes(json.dumps(swap_client.listAllSMSGAddresses(filters)), 'UTF-8')
|
||||||
|
|
||||||
|
|
||||||
def js_rates(self, url_split, post_string, is_json):
|
def js_rates(self, url_split, post_string, is_json) -> bytes:
|
||||||
if post_string == '':
|
post_data = getFormData(post_string, is_json)
|
||||||
raise ValueError('No post data')
|
|
||||||
if is_json:
|
|
||||||
post_data = json.loads(post_string)
|
|
||||||
post_data['is_json'] = True
|
|
||||||
else:
|
|
||||||
post_data = urllib.parse.parse_qs(post_string)
|
|
||||||
|
|
||||||
sc = self.server.swap_client
|
sc = self.server.swap_client
|
||||||
coin_from = get_data_entry(post_data, 'coin_from')
|
coin_from = get_data_entry(post_data, 'coin_from')
|
||||||
@@ -339,7 +448,7 @@ def js_rates(self, url_split, post_string, is_json):
|
|||||||
return bytes(json.dumps(sc.lookupRates(coin_from, coin_to)), 'UTF-8')
|
return bytes(json.dumps(sc.lookupRates(coin_from, coin_to)), 'UTF-8')
|
||||||
|
|
||||||
|
|
||||||
def js_rates_list(self, url_split, query_string, is_json):
|
def js_rates_list(self, url_split, query_string, is_json) -> bytes:
|
||||||
get_data = urllib.parse.parse_qs(query_string)
|
get_data = urllib.parse.parse_qs(query_string)
|
||||||
|
|
||||||
sc = self.server.swap_client
|
sc = self.server.swap_client
|
||||||
@@ -348,14 +457,8 @@ def js_rates_list(self, url_split, query_string, is_json):
|
|||||||
return bytes(json.dumps(sc.lookupRates(coin_from, coin_to, True)), 'UTF-8')
|
return bytes(json.dumps(sc.lookupRates(coin_from, coin_to, True)), 'UTF-8')
|
||||||
|
|
||||||
|
|
||||||
def js_rate(self, url_split, post_string, is_json):
|
def js_rate(self, url_split, post_string, is_json) -> bytes:
|
||||||
if post_string == '':
|
post_data = getFormData(post_string, is_json)
|
||||||
raise ValueError('No post data')
|
|
||||||
if is_json:
|
|
||||||
post_data = json.loads(post_string)
|
|
||||||
post_data['is_json'] = True
|
|
||||||
else:
|
|
||||||
post_data = urllib.parse.parse_qs(post_string)
|
|
||||||
|
|
||||||
sc = self.server.swap_client
|
sc = self.server.swap_client
|
||||||
coin_from = getCoinType(get_data_entry(post_data, 'coin_from'))
|
coin_from = getCoinType(get_data_entry(post_data, 'coin_from'))
|
||||||
@@ -387,5 +490,263 @@ def js_rate(self, url_split, post_string, is_json):
|
|||||||
return bytes(json.dumps({'rate': rate}), 'UTF-8')
|
return bytes(json.dumps({'rate': rate}), 'UTF-8')
|
||||||
|
|
||||||
|
|
||||||
def js_index(self, url_split, post_string, is_json):
|
def js_index(self, url_split, post_string, is_json) -> bytes:
|
||||||
return bytes(json.dumps(self.server.swap_client.getSummary()), 'UTF-8')
|
swap_client = self.server.swap_client
|
||||||
|
swap_client.checkSystemStatus()
|
||||||
|
return bytes(json.dumps(swap_client.getSummary()), 'UTF-8')
|
||||||
|
|
||||||
|
|
||||||
|
def js_generatenotification(self, url_split, post_string, is_json) -> bytes:
|
||||||
|
swap_client = self.server.swap_client
|
||||||
|
|
||||||
|
if not swap_client.debug:
|
||||||
|
raise ValueError('Debug mode not active.')
|
||||||
|
|
||||||
|
r = random.randint(0, 3)
|
||||||
|
if r == 0:
|
||||||
|
swap_client.notify(NT.OFFER_RECEIVED, {'offer_id': random.randbytes(28).hex()})
|
||||||
|
elif r == 1:
|
||||||
|
swap_client.notify(NT.BID_RECEIVED, {'type': 'atomic', 'bid_id': random.randbytes(28).hex(), 'offer_id': random.randbytes(28).hex()})
|
||||||
|
elif r == 2:
|
||||||
|
swap_client.notify(NT.BID_ACCEPTED, {'bid_id': random.randbytes(28).hex()})
|
||||||
|
elif r == 3:
|
||||||
|
swap_client.notify(NT.BID_RECEIVED, {'type': 'ads', 'bid_id': random.randbytes(28).hex(), 'offer_id': random.randbytes(28).hex()})
|
||||||
|
|
||||||
|
return bytes(json.dumps({'type': r}), 'UTF-8')
|
||||||
|
|
||||||
|
|
||||||
|
def js_notifications(self, url_split, post_string, is_json) -> bytes:
|
||||||
|
swap_client = self.server.swap_client
|
||||||
|
swap_client.checkSystemStatus()
|
||||||
|
|
||||||
|
return bytes(json.dumps(swap_client.getNotifications()), 'UTF-8')
|
||||||
|
|
||||||
|
|
||||||
|
def js_identities(self, url_split, post_string: str, is_json: bool) -> bytes:
|
||||||
|
swap_client = self.server.swap_client
|
||||||
|
swap_client.checkSystemStatus()
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
'page_no': 1,
|
||||||
|
'limit': PAGE_LIMIT,
|
||||||
|
'sort_by': 'created_at',
|
||||||
|
'sort_dir': 'desc',
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(url_split) > 3:
|
||||||
|
address = url_split[3]
|
||||||
|
filters['address'] = address
|
||||||
|
|
||||||
|
if post_string != '':
|
||||||
|
post_data = getFormData(post_string, is_json)
|
||||||
|
|
||||||
|
if have_data_entry(post_data, 'sort_by'):
|
||||||
|
sort_by = get_data_entry(post_data, 'sort_by')
|
||||||
|
assert (sort_by in ['created_at', 'rate']), 'Invalid sort by'
|
||||||
|
filters['sort_by'] = sort_by
|
||||||
|
if have_data_entry(post_data, 'sort_dir'):
|
||||||
|
sort_dir = get_data_entry(post_data, 'sort_dir')
|
||||||
|
assert (sort_dir in ['asc', 'desc']), 'Invalid sort dir'
|
||||||
|
filters['sort_dir'] = sort_dir
|
||||||
|
|
||||||
|
if have_data_entry(post_data, 'offset'):
|
||||||
|
filters['offset'] = int(get_data_entry(post_data, 'offset'))
|
||||||
|
if have_data_entry(post_data, 'limit'):
|
||||||
|
filters['limit'] = int(get_data_entry(post_data, 'limit'))
|
||||||
|
assert (filters['limit'] > 0 and filters['limit'] <= PAGE_LIMIT), 'Invalid limit'
|
||||||
|
|
||||||
|
set_data = {}
|
||||||
|
if have_data_entry(post_data, 'set_label'):
|
||||||
|
set_data['label'] = get_data_entry(post_data, 'set_label')
|
||||||
|
if have_data_entry(post_data, 'set_automation_override'):
|
||||||
|
set_data['automation_override'] = get_data_entry(post_data, 'set_automation_override')
|
||||||
|
if have_data_entry(post_data, 'set_visibility_override'):
|
||||||
|
set_data['visibility_override'] = get_data_entry(post_data, 'set_visibility_override')
|
||||||
|
if have_data_entry(post_data, 'set_note'):
|
||||||
|
set_data['note'] = get_data_entry(post_data, 'set_note')
|
||||||
|
|
||||||
|
if set_data:
|
||||||
|
ensure('address' in filters, 'Must provide an address to modify data')
|
||||||
|
swap_client.setIdentityData(filters, set_data)
|
||||||
|
|
||||||
|
return bytes(json.dumps(swap_client.listIdentities(filters)), 'UTF-8')
|
||||||
|
|
||||||
|
|
||||||
|
def js_automationstrategies(self, url_split, post_string: str, is_json: bool) -> bytes:
|
||||||
|
swap_client = self.server.swap_client
|
||||||
|
swap_client.checkSystemStatus()
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
'page_no': 1,
|
||||||
|
'limit': PAGE_LIMIT,
|
||||||
|
'sort_by': 'created_at',
|
||||||
|
'sort_dir': 'desc',
|
||||||
|
}
|
||||||
|
|
||||||
|
if post_string != '':
|
||||||
|
post_data = getFormData(post_string, is_json)
|
||||||
|
|
||||||
|
if have_data_entry(post_data, 'sort_by'):
|
||||||
|
sort_by = get_data_entry(post_data, 'sort_by')
|
||||||
|
assert (sort_by in ['created_at', 'rate']), 'Invalid sort by'
|
||||||
|
filters['sort_by'] = sort_by
|
||||||
|
if have_data_entry(post_data, 'sort_dir'):
|
||||||
|
sort_dir = get_data_entry(post_data, 'sort_dir')
|
||||||
|
assert (sort_dir in ['asc', 'desc']), 'Invalid sort dir'
|
||||||
|
filters['sort_dir'] = sort_dir
|
||||||
|
|
||||||
|
if have_data_entry(post_data, 'offset'):
|
||||||
|
filters['offset'] = int(get_data_entry(post_data, 'offset'))
|
||||||
|
if have_data_entry(post_data, 'limit'):
|
||||||
|
filters['limit'] = int(get_data_entry(post_data, 'limit'))
|
||||||
|
assert (filters['limit'] > 0 and filters['limit'] <= PAGE_LIMIT), 'Invalid limit'
|
||||||
|
|
||||||
|
if len(url_split) > 3:
|
||||||
|
strat_id = int(url_split[3])
|
||||||
|
strat_data = swap_client.getAutomationStrategy(strat_id)
|
||||||
|
rv = {
|
||||||
|
'record_id': strat_data.record_id,
|
||||||
|
'label': strat_data.label,
|
||||||
|
'type_ind': strat_data.type_ind,
|
||||||
|
'only_known_identities': strat_data.only_known_identities,
|
||||||
|
'num_concurrent': strat_data.num_concurrent,
|
||||||
|
'data': json.loads(strat_data.data.decode('utf-8')),
|
||||||
|
'note': '' if strat_data.note is None else strat_data.note,
|
||||||
|
}
|
||||||
|
return bytes(json.dumps(rv), 'UTF-8')
|
||||||
|
|
||||||
|
rv = []
|
||||||
|
strats = swap_client.listAutomationStrategies(filters)
|
||||||
|
for row in strats:
|
||||||
|
rv.append((row[0], row[1], row[2]))
|
||||||
|
return bytes(json.dumps(rv), 'UTF-8')
|
||||||
|
|
||||||
|
|
||||||
|
def js_vacuumdb(self, url_split, post_string, is_json) -> bytes:
|
||||||
|
swap_client = self.server.swap_client
|
||||||
|
swap_client.checkSystemStatus()
|
||||||
|
swap_client.vacuumDB()
|
||||||
|
|
||||||
|
return bytes(json.dumps({'completed': True}), 'UTF-8')
|
||||||
|
|
||||||
|
|
||||||
|
def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
|
||||||
|
swap_client = self.server.swap_client
|
||||||
|
swap_client.checkSystemStatus()
|
||||||
|
post_data = getFormData(post_string, is_json)
|
||||||
|
|
||||||
|
coin = getCoinType(get_data_entry(post_data, 'coin'))
|
||||||
|
if coin in (Coins.PART, Coins.PART_ANON, Coins.PART_BLIND):
|
||||||
|
raise ValueError('Particl wallet seed is set from the Basicswap mnemonic.')
|
||||||
|
|
||||||
|
ci = swap_client.ci(coin)
|
||||||
|
if coin == Coins.XMR:
|
||||||
|
key_view = swap_client.getWalletKey(coin, 1, for_ed25519=True)
|
||||||
|
key_spend = swap_client.getWalletKey(coin, 2, for_ed25519=True)
|
||||||
|
address = ci.getAddressFromKeys(key_view, key_spend)
|
||||||
|
return bytes(json.dumps({'coin': ci.ticker(), 'key_view': ci.encodeKey(key_view), 'key_spend': ci.encodeKey(key_spend), 'address': address}), 'UTF-8')
|
||||||
|
|
||||||
|
seed_key = swap_client.getWalletKey(coin, 1)
|
||||||
|
if coin == Coins.DASH:
|
||||||
|
return bytes(json.dumps({'coin': ci.ticker(), 'seed': seed_key.hex(), 'mnemonic': ci.seedToMnemonic(seed_key)}), 'UTF-8')
|
||||||
|
seed_id = ci.getSeedHash(seed_key)
|
||||||
|
return bytes(json.dumps({'coin': ci.ticker(), 'seed': seed_key.hex(), 'seed_id': seed_id.hex()}), 'UTF-8')
|
||||||
|
|
||||||
|
|
||||||
|
def js_setpassword(self, url_split, post_string, is_json) -> bytes:
|
||||||
|
# Set or change wallet passwords
|
||||||
|
# Only works with currently enabled coins
|
||||||
|
# Will fail if any coin does not unlock on the old password
|
||||||
|
swap_client = self.server.swap_client
|
||||||
|
post_data = getFormData(post_string, is_json)
|
||||||
|
|
||||||
|
old_password = get_data_entry(post_data, 'oldpassword')
|
||||||
|
new_password = get_data_entry(post_data, 'newpassword')
|
||||||
|
|
||||||
|
if have_data_entry(post_data, 'coin'):
|
||||||
|
# Set password for one coin
|
||||||
|
coin = getCoinType(get_data_entry(post_data, 'coin'))
|
||||||
|
if coin in (Coins.PART_ANON, Coins.PART_BLIND, Coins.LTC_MWEB):
|
||||||
|
raise ValueError('Invalid coin.')
|
||||||
|
swap_client.changeWalletPasswords(old_password, new_password, coin)
|
||||||
|
return bytes(json.dumps({'success': True}), 'UTF-8')
|
||||||
|
|
||||||
|
# Set password for all coins
|
||||||
|
swap_client.changeWalletPasswords(old_password, new_password)
|
||||||
|
return bytes(json.dumps({'success': True}), 'UTF-8')
|
||||||
|
|
||||||
|
|
||||||
|
def js_unlock(self, url_split, post_string, is_json) -> bytes:
|
||||||
|
swap_client = self.server.swap_client
|
||||||
|
post_data = getFormData(post_string, is_json)
|
||||||
|
|
||||||
|
password = get_data_entry(post_data, 'password')
|
||||||
|
|
||||||
|
if have_data_entry(post_data, 'coin'):
|
||||||
|
coin = getCoinType(str(get_data_entry(post_data, 'coin')))
|
||||||
|
if coin in (Coins.PART_ANON, Coins.PART_BLIND):
|
||||||
|
raise ValueError('Invalid coin.')
|
||||||
|
swap_client.unlockWallets(password, coin)
|
||||||
|
return bytes(json.dumps({'success': True}), 'UTF-8')
|
||||||
|
|
||||||
|
swap_client.unlockWallets(password)
|
||||||
|
return bytes(json.dumps({'success': True}), 'UTF-8')
|
||||||
|
|
||||||
|
|
||||||
|
def js_lock(self, url_split, post_string, is_json) -> bytes:
|
||||||
|
swap_client = self.server.swap_client
|
||||||
|
post_data = {} if post_string == '' else getFormData(post_string, is_json)
|
||||||
|
|
||||||
|
if have_data_entry(post_data, 'coin'):
|
||||||
|
coin = getCoinType(get_data_entry(post_data, 'coin'))
|
||||||
|
if coin in (Coins.PART_ANON, Coins.PART_BLIND):
|
||||||
|
raise ValueError('Invalid coin.')
|
||||||
|
swap_client.lockWallets(coin)
|
||||||
|
return bytes(json.dumps({'success': True}), 'UTF-8')
|
||||||
|
|
||||||
|
swap_client.lockWallets()
|
||||||
|
return bytes(json.dumps({'success': True}), 'UTF-8')
|
||||||
|
|
||||||
|
|
||||||
|
def js_404(self, url_split, post_string, is_json) -> bytes:
|
||||||
|
return bytes(json.dumps({'Error': 'path unknown'}), 'UTF-8')
|
||||||
|
|
||||||
|
|
||||||
|
def js_help(self, url_split, post_string, is_json) -> bytes:
|
||||||
|
# TODO: Add details and examples
|
||||||
|
commands = []
|
||||||
|
for k in pages:
|
||||||
|
commands.append(k)
|
||||||
|
return bytes(json.dumps({'commands': commands}), 'UTF-8')
|
||||||
|
|
||||||
|
|
||||||
|
pages = {
|
||||||
|
'coins': js_coins,
|
||||||
|
'wallets': js_wallets,
|
||||||
|
'offers': js_offers,
|
||||||
|
'sentoffers': js_sentoffers,
|
||||||
|
'bids': js_bids,
|
||||||
|
'sentbids': js_sentbids,
|
||||||
|
'network': js_network,
|
||||||
|
'revokeoffer': js_revokeoffer,
|
||||||
|
'smsgaddresses': js_smsgaddresses,
|
||||||
|
'rate': js_rate,
|
||||||
|
'rates': js_rates,
|
||||||
|
'rateslist': js_rates_list,
|
||||||
|
'generatenotification': js_generatenotification,
|
||||||
|
'notifications': js_notifications,
|
||||||
|
'identities': js_identities,
|
||||||
|
'automationstrategies': js_automationstrategies,
|
||||||
|
'vacuumdb': js_vacuumdb,
|
||||||
|
'getcoinseed': js_getcoinseed,
|
||||||
|
'setpassword': js_setpassword,
|
||||||
|
'unlock': js_unlock,
|
||||||
|
'lock': js_lock,
|
||||||
|
'help': js_help,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def js_url_to_function(url_split):
|
||||||
|
if len(url_split) > 2:
|
||||||
|
return pages.get(url_split[2], js_404)
|
||||||
|
return js_index
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ message OfferMessage {
|
|||||||
uint32 protocol_version = 16;
|
uint32 protocol_version = 16;
|
||||||
bool amount_negotiable = 17;
|
bool amount_negotiable = 17;
|
||||||
bool rate_negotiable = 18;
|
bool rate_negotiable = 18;
|
||||||
|
|
||||||
|
bytes proof_utxos = 19; /* 32 byte txid 2 byte vout, repeated */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Step 2, buyer -> seller */
|
/* Step 2, buyer -> seller */
|
||||||
@@ -40,8 +42,21 @@ message BidMessage {
|
|||||||
bytes offer_msg_id = 1;
|
bytes offer_msg_id = 1;
|
||||||
uint64 time_valid = 2; /* seconds bid is valid for */
|
uint64 time_valid = 2; /* seconds bid is valid for */
|
||||||
uint64 amount = 3; /* amount of amount_from bid is for */
|
uint64 amount = 3; /* amount of amount_from bid is for */
|
||||||
|
uint64 rate = 4;
|
||||||
|
bytes pkhash_buyer = 5; /* buyer's address to receive amount_from */
|
||||||
|
string proof_address = 6;
|
||||||
|
string proof_signature = 7;
|
||||||
|
|
||||||
/* optional */
|
uint32 protocol_version = 8;
|
||||||
|
|
||||||
|
bytes proof_utxos = 9; /* 32 byte txid 2 byte vout, repeated */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For tests */
|
||||||
|
message BidMessage_v1Deprecated {
|
||||||
|
bytes offer_msg_id = 1;
|
||||||
|
uint64 time_valid = 2; /* seconds bid is valid for */
|
||||||
|
uint64 amount = 3; /* amount of amount_from bid is for */
|
||||||
uint64 rate = 4;
|
uint64 rate = 4;
|
||||||
bytes pkhash_buyer = 5; /* buyer's address to receive amount_from */
|
bytes pkhash_buyer = 5; /* buyer's address to receive amount_from */
|
||||||
string proof_address = 6;
|
string proof_address = 6;
|
||||||
@@ -50,6 +65,8 @@ message BidMessage {
|
|||||||
uint32 protocol_version = 8;
|
uint32 protocol_version = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Step 3, seller -> buyer */
|
/* Step 3, seller -> buyer */
|
||||||
message BidAcceptMessage {
|
message BidAcceptMessage {
|
||||||
bytes bid_msg_id = 1;
|
bytes bid_msg_id = 1;
|
||||||
@@ -131,3 +148,24 @@ message XmrBidLockReleaseMessage {
|
|||||||
|
|
||||||
bytes al_lock_spend_tx_esig = 2;
|
bytes al_lock_spend_tx_esig = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message ADSBidIntentMessage {
|
||||||
|
/* L -> F Sent from bidder, construct a reverse bid */
|
||||||
|
bytes offer_msg_id = 1;
|
||||||
|
uint64 time_valid = 2; /* seconds bid is valid for */
|
||||||
|
uint64 amount_from = 3; /* amount of offer.coin_from bid is for */
|
||||||
|
uint64 amount_to = 4; /* amount of offer.coin_to bid is for, equivalent to bid.amount */
|
||||||
|
uint64 rate = 5; /* amount of offer.coin_from bid is for */
|
||||||
|
|
||||||
|
uint32 protocol_version = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ADSBidIntentAcceptMessage {
|
||||||
|
/* F -> L Sent from offerer, construct a reverse bid */
|
||||||
|
bytes bid_msg_id = 1;
|
||||||
|
|
||||||
|
bytes pkaf = 2;
|
||||||
|
bytes kbvf = 3;
|
||||||
|
bytes kbsf_dleag = 4;
|
||||||
|
bytes dest_af = 5;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||||
# source: messages.proto
|
# source: messages.proto
|
||||||
"""Generated protocol buffer code."""
|
"""Generated protocol buffer code."""
|
||||||
from google.protobuf.internal import builder as _builder
|
|
||||||
from google.protobuf import descriptor as _descriptor
|
from google.protobuf import descriptor as _descriptor
|
||||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||||
from google.protobuf import symbol_database as _symbol_database
|
from google.protobuf import symbol_database as _symbol_database
|
||||||
|
from google.protobuf.internal import builder as _builder
|
||||||
# @@protoc_insertion_point(imports)
|
# @@protoc_insertion_point(imports)
|
||||||
|
|
||||||
_sym_db = _symbol_database.Default()
|
_sym_db = _symbol_database.Default()
|
||||||
@@ -13,35 +13,41 @@ _sym_db = _symbol_database.Default()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0emessages.proto\x12\tbasicswap\"\xa6\x04\n\x0cOfferMessage\x12\x11\n\tcoin_from\x18\x01 \x01(\r\x12\x0f\n\x07\x63oin_to\x18\x02 \x01(\r\x12\x13\n\x0b\x61mount_from\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x16\n\x0emin_bid_amount\x18\x05 \x01(\x04\x12\x12\n\ntime_valid\x18\x06 \x01(\x04\x12\x33\n\tlock_type\x18\x07 \x01(\x0e\x32 .basicswap.OfferMessage.LockType\x12\x12\n\nlock_value\x18\x08 \x01(\r\x12\x11\n\tswap_type\x18\t \x01(\r\x12\x15\n\rproof_address\x18\n \x01(\t\x12\x17\n\x0fproof_signature\x18\x0b \x01(\t\x12\x15\n\rpkhash_seller\x18\x0c \x01(\x0c\x12\x13\n\x0bsecret_hash\x18\r \x01(\x0c\x12\x15\n\rfee_rate_from\x18\x0e \x01(\x04\x12\x13\n\x0b\x66\x65\x65_rate_to\x18\x0f \x01(\x04\x12\x18\n\x10protocol_version\x18\x10 \x01(\r\x12\x19\n\x11\x61mount_negotiable\x18\x11 \x01(\x08\x12\x17\n\x0frate_negotiable\x18\x12 \x01(\x08\"q\n\x08LockType\x12\x0b\n\x07NOT_SET\x10\x00\x12\x18\n\x14SEQUENCE_LOCK_BLOCKS\x10\x01\x12\x16\n\x12SEQUENCE_LOCK_TIME\x10\x02\x12\x13\n\x0f\x41\x42S_LOCK_BLOCKS\x10\x03\x12\x11\n\rABS_LOCK_TIME\x10\x04\"\xb4\x01\n\nBidMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x14\n\x0cpkhash_buyer\x18\x05 \x01(\x0c\x12\x15\n\rproof_address\x18\x06 \x01(\t\x12\x17\n\x0fproof_signature\x18\x07 \x01(\t\x12\x18\n\x10protocol_version\x18\x08 \x01(\r\"V\n\x10\x42idAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x15\n\rinitiate_txid\x18\x02 \x01(\x0c\x12\x17\n\x0f\x63ontract_script\x18\x03 \x01(\x0c\"=\n\x12OfferRevokeMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\";\n\x10\x42idRejectMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x13\n\x0breject_code\x18\x02 \x01(\r\"\xb2\x01\n\rXmrBidMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x0c\n\x04pkaf\x18\x05 \x01(\x0c\x12\x0c\n\x04kbvf\x18\x06 \x01(\x0c\x12\x12\n\nkbsf_dleag\x18\x07 \x01(\x0c\x12\x0f\n\x07\x64\x65st_af\x18\x08 \x01(\x0c\x12\x18\n\x10protocol_version\x18\t \x01(\r\"T\n\x0fXmrSplitMessage\x12\x0e\n\x06msg_id\x18\x01 \x01(\x0c\x12\x10\n\x08msg_type\x18\x02 \x01(\r\x12\x10\n\x08sequence\x18\x03 \x01(\r\x12\r\n\x05\x64leag\x18\x04 \x01(\x0c\"\x80\x02\n\x13XmrBidAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x0c\n\x04pkal\x18\x03 \x01(\x0c\x12\x0c\n\x04kbvl\x18\x04 \x01(\x0c\x12\x12\n\nkbsl_dleag\x18\x05 \x01(\x0c\x12\x11\n\ta_lock_tx\x18\x06 \x01(\x0c\x12\x18\n\x10\x61_lock_tx_script\x18\x07 \x01(\x0c\x12\x18\n\x10\x61_lock_refund_tx\x18\x08 \x01(\x0c\x12\x1f\n\x17\x61_lock_refund_tx_script\x18\t \x01(\x0c\x12\x1e\n\x16\x61_lock_refund_spend_tx\x18\n \x01(\x0c\x12\x1d\n\x15\x61l_lock_refund_tx_sig\x18\x0b \x01(\x0c\"r\n\x17XmrBidLockTxSigsMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12$\n\x1c\x61\x66_lock_refund_spend_tx_esig\x18\x02 \x01(\x0c\x12\x1d\n\x15\x61\x66_lock_refund_tx_sig\x18\x03 \x01(\x0c\"X\n\x18XmrBidLockSpendTxMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x17\n\x0f\x61_lock_spend_tx\x18\x02 \x01(\x0c\x12\x0f\n\x07kal_sig\x18\x03 \x01(\x0c\"M\n\x18XmrBidLockReleaseMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x1d\n\x15\x61l_lock_spend_tx_esig\x18\x02 \x01(\x0c\x62\x06proto3')
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0emessages.proto\x12\tbasicswap\"\xa6\x04\n\x0cOfferMessage\x12\x11\n\tcoin_from\x18\x01 \x01(\r\x12\x0f\n\x07\x63oin_to\x18\x02 \x01(\r\x12\x13\n\x0b\x61mount_from\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x16\n\x0emin_bid_amount\x18\x05 \x01(\x04\x12\x12\n\ntime_valid\x18\x06 \x01(\x04\x12\x33\n\tlock_type\x18\x07 \x01(\x0e\x32 .basicswap.OfferMessage.LockType\x12\x12\n\nlock_value\x18\x08 \x01(\r\x12\x11\n\tswap_type\x18\t \x01(\r\x12\x15\n\rproof_address\x18\n \x01(\t\x12\x17\n\x0fproof_signature\x18\x0b \x01(\t\x12\x15\n\rpkhash_seller\x18\x0c \x01(\x0c\x12\x13\n\x0bsecret_hash\x18\r \x01(\x0c\x12\x15\n\rfee_rate_from\x18\x0e \x01(\x04\x12\x13\n\x0b\x66\x65\x65_rate_to\x18\x0f \x01(\x04\x12\x18\n\x10protocol_version\x18\x10 \x01(\r\x12\x19\n\x11\x61mount_negotiable\x18\x11 \x01(\x08\x12\x17\n\x0frate_negotiable\x18\x12 \x01(\x08\"q\n\x08LockType\x12\x0b\n\x07NOT_SET\x10\x00\x12\x18\n\x14SEQUENCE_LOCK_BLOCKS\x10\x01\x12\x16\n\x12SEQUENCE_LOCK_TIME\x10\x02\x12\x13\n\x0f\x41\x42S_LOCK_BLOCKS\x10\x03\x12\x11\n\rABS_LOCK_TIME\x10\x04\"\xc9\x01\n\nBidMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x14\n\x0cpkhash_buyer\x18\x05 \x01(\x0c\x12\x15\n\rproof_address\x18\x06 \x01(\t\x12\x17\n\x0fproof_signature\x18\x07 \x01(\t\x12\x18\n\x10protocol_version\x18\x08 \x01(\r\x12\x13\n\x0bproof_utxos\x18\t \x01(\x0c\"\xc1\x01\n\x17\x42idMessage_v1Deprecated\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x14\n\x0cpkhash_buyer\x18\x05 \x01(\x0c\x12\x15\n\rproof_address\x18\x06 \x01(\t\x12\x17\n\x0fproof_signature\x18\x07 \x01(\t\x12\x18\n\x10protocol_version\x18\x08 \x01(\r\"V\n\x10\x42idAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x15\n\rinitiate_txid\x18\x02 \x01(\x0c\x12\x17\n\x0f\x63ontract_script\x18\x03 \x01(\x0c\"=\n\x12OfferRevokeMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\";\n\x10\x42idRejectMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x13\n\x0breject_code\x18\x02 \x01(\r\"\xb2\x01\n\rXmrBidMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x0c\n\x04pkaf\x18\x05 \x01(\x0c\x12\x0c\n\x04kbvf\x18\x06 \x01(\x0c\x12\x12\n\nkbsf_dleag\x18\x07 \x01(\x0c\x12\x0f\n\x07\x64\x65st_af\x18\x08 \x01(\x0c\x12\x18\n\x10protocol_version\x18\t \x01(\r\"T\n\x0fXmrSplitMessage\x12\x0e\n\x06msg_id\x18\x01 \x01(\x0c\x12\x10\n\x08msg_type\x18\x02 \x01(\r\x12\x10\n\x08sequence\x18\x03 \x01(\r\x12\r\n\x05\x64leag\x18\x04 \x01(\x0c\"\x80\x02\n\x13XmrBidAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x0c\n\x04pkal\x18\x03 \x01(\x0c\x12\x0c\n\x04kbvl\x18\x04 \x01(\x0c\x12\x12\n\nkbsl_dleag\x18\x05 \x01(\x0c\x12\x11\n\ta_lock_tx\x18\x06 \x01(\x0c\x12\x18\n\x10\x61_lock_tx_script\x18\x07 \x01(\x0c\x12\x18\n\x10\x61_lock_refund_tx\x18\x08 \x01(\x0c\x12\x1f\n\x17\x61_lock_refund_tx_script\x18\t \x01(\x0c\x12\x1e\n\x16\x61_lock_refund_spend_tx\x18\n \x01(\x0c\x12\x1d\n\x15\x61l_lock_refund_tx_sig\x18\x0b \x01(\x0c\"r\n\x17XmrBidLockTxSigsMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12$\n\x1c\x61\x66_lock_refund_spend_tx_esig\x18\x02 \x01(\x0c\x12\x1d\n\x15\x61\x66_lock_refund_tx_sig\x18\x03 \x01(\x0c\"X\n\x18XmrBidLockSpendTxMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x17\n\x0f\x61_lock_spend_tx\x18\x02 \x01(\x0c\x12\x0f\n\x07kal_sig\x18\x03 \x01(\x0c\"M\n\x18XmrBidLockReleaseMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x1d\n\x15\x61l_lock_spend_tx_esig\x18\x02 \x01(\x0c\"\x8f\x01\n\x13\x41\x44SBidIntentMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x13\n\x0b\x61mount_from\x18\x03 \x01(\x04\x12\x11\n\tamount_to\x18\x04 \x01(\x04\x12\x0c\n\x04rate\x18\x05 \x01(\x04\x12\x18\n\x10protocol_version\x18\x06 \x01(\r\"p\n\x19\x41\x44SBidIntentAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x0c\n\x04pkaf\x18\x02 \x01(\x0c\x12\x0c\n\x04kbvf\x18\x03 \x01(\x0c\x12\x12\n\nkbsf_dleag\x18\x04 \x01(\x0c\x12\x0f\n\x07\x64\x65st_af\x18\x05 \x01(\x0c\x62\x06proto3')
|
||||||
|
|
||||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
_globals = globals()
|
||||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'messages_pb2', globals())
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||||
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'messages_pb2', _globals)
|
||||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
|
|
||||||
DESCRIPTOR._options = None
|
DESCRIPTOR._options = None
|
||||||
_OFFERMESSAGE._serialized_start=30
|
_globals['_OFFERMESSAGE']._serialized_start=30
|
||||||
_OFFERMESSAGE._serialized_end=580
|
_globals['_OFFERMESSAGE']._serialized_end=580
|
||||||
_OFFERMESSAGE_LOCKTYPE._serialized_start=467
|
_globals['_OFFERMESSAGE_LOCKTYPE']._serialized_start=467
|
||||||
_OFFERMESSAGE_LOCKTYPE._serialized_end=580
|
_globals['_OFFERMESSAGE_LOCKTYPE']._serialized_end=580
|
||||||
_BIDMESSAGE._serialized_start=583
|
_globals['_BIDMESSAGE']._serialized_start=583
|
||||||
_BIDMESSAGE._serialized_end=763
|
_globals['_BIDMESSAGE']._serialized_end=784
|
||||||
_BIDACCEPTMESSAGE._serialized_start=765
|
_globals['_BIDMESSAGE_V1DEPRECATED']._serialized_start=787
|
||||||
_BIDACCEPTMESSAGE._serialized_end=851
|
_globals['_BIDMESSAGE_V1DEPRECATED']._serialized_end=980
|
||||||
_OFFERREVOKEMESSAGE._serialized_start=853
|
_globals['_BIDACCEPTMESSAGE']._serialized_start=982
|
||||||
_OFFERREVOKEMESSAGE._serialized_end=914
|
_globals['_BIDACCEPTMESSAGE']._serialized_end=1068
|
||||||
_BIDREJECTMESSAGE._serialized_start=916
|
_globals['_OFFERREVOKEMESSAGE']._serialized_start=1070
|
||||||
_BIDREJECTMESSAGE._serialized_end=975
|
_globals['_OFFERREVOKEMESSAGE']._serialized_end=1131
|
||||||
_XMRBIDMESSAGE._serialized_start=978
|
_globals['_BIDREJECTMESSAGE']._serialized_start=1133
|
||||||
_XMRBIDMESSAGE._serialized_end=1156
|
_globals['_BIDREJECTMESSAGE']._serialized_end=1192
|
||||||
_XMRSPLITMESSAGE._serialized_start=1158
|
_globals['_XMRBIDMESSAGE']._serialized_start=1195
|
||||||
_XMRSPLITMESSAGE._serialized_end=1242
|
_globals['_XMRBIDMESSAGE']._serialized_end=1373
|
||||||
_XMRBIDACCEPTMESSAGE._serialized_start=1245
|
_globals['_XMRSPLITMESSAGE']._serialized_start=1375
|
||||||
_XMRBIDACCEPTMESSAGE._serialized_end=1501
|
_globals['_XMRSPLITMESSAGE']._serialized_end=1459
|
||||||
_XMRBIDLOCKTXSIGSMESSAGE._serialized_start=1503
|
_globals['_XMRBIDACCEPTMESSAGE']._serialized_start=1462
|
||||||
_XMRBIDLOCKTXSIGSMESSAGE._serialized_end=1617
|
_globals['_XMRBIDACCEPTMESSAGE']._serialized_end=1718
|
||||||
_XMRBIDLOCKSPENDTXMESSAGE._serialized_start=1619
|
_globals['_XMRBIDLOCKTXSIGSMESSAGE']._serialized_start=1720
|
||||||
_XMRBIDLOCKSPENDTXMESSAGE._serialized_end=1707
|
_globals['_XMRBIDLOCKTXSIGSMESSAGE']._serialized_end=1834
|
||||||
_XMRBIDLOCKRELEASEMESSAGE._serialized_start=1709
|
_globals['_XMRBIDLOCKSPENDTXMESSAGE']._serialized_start=1836
|
||||||
_XMRBIDLOCKRELEASEMESSAGE._serialized_end=1786
|
_globals['_XMRBIDLOCKSPENDTXMESSAGE']._serialized_end=1924
|
||||||
|
_globals['_XMRBIDLOCKRELEASEMESSAGE']._serialized_start=1926
|
||||||
|
_globals['_XMRBIDLOCKRELEASEMESSAGE']._serialized_end=2003
|
||||||
|
_globals['_ADSBIDINTENTMESSAGE']._serialized_start=2006
|
||||||
|
_globals['_ADSBIDINTENTMESSAGE']._serialized_end=2149
|
||||||
|
_globals['_ADSBIDINTENTACCEPTMESSAGE']._serialized_start=2151
|
||||||
|
_globals['_ADSBIDINTENTACCEPTMESSAGE']._serialized_end=2263
|
||||||
# @@protoc_insertion_point(module_scope)
|
# @@protoc_insertion_point(module_scope)
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import queue
|
|||||||
import random
|
import random
|
||||||
import select
|
import select
|
||||||
import socket
|
import socket
|
||||||
import struct
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import secrets
|
import secrets
|
||||||
@@ -41,7 +40,7 @@ from basicswap.contrib.rfc6979 import (
|
|||||||
|
|
||||||
|
|
||||||
START_TOKEN = 0xabcd
|
START_TOKEN = 0xabcd
|
||||||
MSG_START_TOKEN = struct.pack('>H', START_TOKEN)
|
MSG_START_TOKEN = START_TOKEN.to_bytes(2, 'big')
|
||||||
|
|
||||||
MSG_MAX_SIZE = 0x200000 # 2MB
|
MSG_MAX_SIZE = 0x200000 # 2MB
|
||||||
|
|
||||||
@@ -83,8 +82,8 @@ class MsgHandshake:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def encode_aad(self): # Additional Authenticated Data
|
def encode_aad(self): # Additional Authenticated Data
|
||||||
return struct.pack('>H', NetMessageTypes.HANDSHAKE) + \
|
return int(NetMessageTypes.HANDSHAKE).to_bytes(2, 'big') + \
|
||||||
struct.pack('>Q', self._timestamp) + \
|
self._timestamp.to_bytes(8, 'big') + \
|
||||||
self._ephem_pk
|
self._ephem_pk
|
||||||
|
|
||||||
def encode(self):
|
def encode(self):
|
||||||
@@ -92,7 +91,7 @@ class MsgHandshake:
|
|||||||
|
|
||||||
def decode(self, msg_mv):
|
def decode(self, msg_mv):
|
||||||
o = 2
|
o = 2
|
||||||
self._timestamp = struct.unpack('>Q', msg_mv[o: o + 8])[0]
|
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
|
||||||
@@ -333,7 +332,7 @@ class Network:
|
|||||||
|
|
||||||
ss = k.ecdh(peer._pubkey)
|
ss = k.ecdh(peer._pubkey)
|
||||||
|
|
||||||
hashed = hashlib.sha512(ss + struct.pack('>Q', msg._timestamp)).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:]
|
||||||
|
|
||||||
@@ -386,7 +385,7 @@ class Network:
|
|||||||
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 + struct.pack('>Q', msg._timestamp)).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:]
|
||||||
|
|
||||||
@@ -427,7 +426,7 @@ 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 = struct.unpack('>I', plaintext[:4])[0]
|
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
|
||||||
@@ -450,7 +449,7 @@ 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 = struct.unpack('>I', plaintext[:4])[0]
|
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
|
||||||
@@ -462,14 +461,14 @@ class Network:
|
|||||||
def send_ping(self, peer):
|
def send_ping(self, peer):
|
||||||
ping_nonce = random.getrandbits(32)
|
ping_nonce = random.getrandbits(32)
|
||||||
|
|
||||||
msg_bytes = struct.pack('>H', NetMessageTypes.PING)
|
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 = struct.pack('>I', ping_nonce)
|
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)
|
||||||
@@ -484,14 +483,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 = struct.pack('>H', NetMessageTypes.PONG)
|
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 = struct.pack('>I', ping_nonce)
|
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
|
||||||
|
|
||||||
@@ -503,7 +502,7 @@ 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) + struct.pack('>I', len_encoded) + 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
|
||||||
@@ -515,7 +514,7 @@ class Network:
|
|||||||
try:
|
try:
|
||||||
mv = memoryview(msg_bytes)
|
mv = memoryview(msg_bytes)
|
||||||
o = 0
|
o = 0
|
||||||
msg_type = struct.unpack('>H', mv[o: o + 2])[0]
|
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:
|
||||||
@@ -548,13 +547,13 @@ class Network:
|
|||||||
raise ValueError('Invalid start token')
|
raise ValueError('Invalid start token')
|
||||||
o += 2
|
o += 2
|
||||||
|
|
||||||
msg_len = struct.unpack('>I', mv[o: o + 4])[0]
|
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 = struct.unpack('>H', mv[o: o + 2])[0]
|
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')
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2022-2023 tecnovert
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
|
from basicswap.script import (
|
||||||
|
OpCodes,
|
||||||
|
)
|
||||||
|
from basicswap.interface.btc import (
|
||||||
|
find_vout_for_address_from_txobj,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProtocolInterface:
|
||||||
|
swap_type = None
|
||||||
|
|
||||||
|
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
|
||||||
|
raise ValueError('base class')
|
||||||
|
|
||||||
|
def getMockScript(self) -> bytearray:
|
||||||
|
return bytearray([
|
||||||
|
OpCodes.OP_RETURN, OpCodes.OP_1])
|
||||||
|
|
||||||
|
def getMockScriptScriptPubkey(self, ci) -> bytearray:
|
||||||
|
script = self.getMockScript()
|
||||||
|
return ci.getScriptDest(script) if ci._use_segwit else ci.get_p2sh_script_pubkey(script)
|
||||||
|
|
||||||
|
def getMockAddrTo(self, ci):
|
||||||
|
script = self.getMockScript()
|
||||||
|
return ci.encodeScriptDest(ci.getScriptDest(script)) if ci._use_segwit else ci.encode_p2sh(script)
|
||||||
|
|
||||||
|
def findMockVout(self, ci, itx_decoded):
|
||||||
|
mock_addr = self.getMockAddrTo(ci)
|
||||||
|
return find_vout_for_address_from_txobj(itx_decoded, mock_addr)
|
||||||
|
|||||||
@@ -1,20 +1,29 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2020-2022 tecnovert
|
# Copyright (c) 2020-2023 tecnovert
|
||||||
# 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.
|
||||||
|
|
||||||
|
from basicswap.db import (
|
||||||
|
Concepts,
|
||||||
|
)
|
||||||
from basicswap.util import (
|
from basicswap.util import (
|
||||||
SerialiseNum,
|
SerialiseNum,
|
||||||
)
|
)
|
||||||
from basicswap.script import (
|
from basicswap.script import (
|
||||||
OpCodes,
|
OpCodes,
|
||||||
)
|
)
|
||||||
|
from basicswap.basicswap_util import (
|
||||||
|
SwapTypes,
|
||||||
|
EventLogTypes,
|
||||||
|
)
|
||||||
|
from . import ProtocolInterface
|
||||||
|
|
||||||
INITIATE_TX_TIMEOUT = 40 * 60 # TODO: make variable per coin
|
INITIATE_TX_TIMEOUT = 40 * 60 # TODO: make variable per coin
|
||||||
|
ABS_LOCK_TIME_LEEWAY = 10 * 60
|
||||||
|
|
||||||
|
|
||||||
def buildContractScript(lock_val, secret_hash, pkh_redeem, pkh_refund, op_lock=OpCodes.OP_CHECKSEQUENCEVERIFY):
|
def buildContractScript(lock_val: int, secret_hash: bytes, pkh_redeem: bytes, pkh_refund: bytes, op_lock=OpCodes.OP_CHECKSEQUENCEVERIFY) -> bytearray:
|
||||||
script = bytearray([
|
script = bytearray([
|
||||||
OpCodes.OP_IF,
|
OpCodes.OP_IF,
|
||||||
OpCodes.OP_SIZE,
|
OpCodes.OP_SIZE,
|
||||||
@@ -49,7 +58,7 @@ def extractScriptSecretHash(script):
|
|||||||
return script[7:39]
|
return script[7:39]
|
||||||
|
|
||||||
|
|
||||||
def redeemITx(self, bid_id, session):
|
def redeemITx(self, bid_id: bytes, session):
|
||||||
bid, offer = self.getBidAndOffer(bid_id, session)
|
bid, offer = self.getBidAndOffer(bid_id, session)
|
||||||
ci_from = self.ci(offer.coin_from)
|
ci_from = self.ci(offer.coin_from)
|
||||||
|
|
||||||
@@ -58,3 +67,34 @@ def redeemITx(self, bid_id, session):
|
|||||||
|
|
||||||
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('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, '', session)
|
||||||
|
|
||||||
|
|
||||||
|
class AtomicSwapInterface(ProtocolInterface):
|
||||||
|
swap_type = SwapTypes.SELLER_FIRST
|
||||||
|
|
||||||
|
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
|
||||||
|
addr_to = self.getMockAddrTo(ci)
|
||||||
|
funded_tx = ci.createRawFundedTransaction(addr_to, amount, sub_fee, lock_unspents=False)
|
||||||
|
|
||||||
|
return bytes.fromhex(funded_tx)
|
||||||
|
|
||||||
|
def promoteMockTx(self, ci, mock_tx: bytes, script: bytearray) -> bytearray:
|
||||||
|
mock_txo_script = self.getMockScriptScriptPubkey(ci)
|
||||||
|
real_txo_script = ci.getScriptDest(script) if ci._use_segwit else ci.get_p2sh_script_pubkey(script)
|
||||||
|
|
||||||
|
found: int = 0
|
||||||
|
ctx = ci.loadTx(mock_tx)
|
||||||
|
for txo in ctx.vout:
|
||||||
|
if txo.scriptPubKey == mock_txo_script:
|
||||||
|
txo.scriptPubKey = real_txo_script
|
||||||
|
found += 1
|
||||||
|
|
||||||
|
if found < 1:
|
||||||
|
raise ValueError('Mocked output not found')
|
||||||
|
if found > 1:
|
||||||
|
raise ValueError('Too many mocked outputs found')
|
||||||
|
ctx.nLockTime = 0
|
||||||
|
|
||||||
|
funded_tx = ctx.serialize()
|
||||||
|
return ci.signTxWithWallet(funded_tx)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2020 tecnovert
|
# Copyright (c) 2020-2023 tecnovert
|
||||||
# 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.
|
||||||
|
|
||||||
@@ -9,13 +9,19 @@ from sqlalchemy.orm import scoped_session
|
|||||||
from basicswap.util import (
|
from basicswap.util import (
|
||||||
ensure,
|
ensure,
|
||||||
)
|
)
|
||||||
|
from basicswap.interface import Curves
|
||||||
from basicswap.chainparams import (
|
from basicswap.chainparams import (
|
||||||
Coins,
|
Coins,
|
||||||
)
|
)
|
||||||
from basicswap.basicswap_util import (
|
from basicswap.basicswap_util import (
|
||||||
KeyTypes,
|
KeyTypes,
|
||||||
|
SwapTypes,
|
||||||
EventLogTypes,
|
EventLogTypes,
|
||||||
)
|
)
|
||||||
|
from . import ProtocolInterface
|
||||||
|
from basicswap.contrib.test_framework.script import (
|
||||||
|
CScript, CScriptOp,
|
||||||
|
OP_CHECKMULTISIG)
|
||||||
|
|
||||||
|
|
||||||
def addLockRefundSigs(self, xmr_swap, ci):
|
def addLockRefundSigs(self, xmr_swap, ci):
|
||||||
@@ -32,17 +38,17 @@ def addLockRefundSigs(self, xmr_swap, ci):
|
|||||||
xmr_swap.a_lock_refund_tx = signed_tx
|
xmr_swap.a_lock_refund_tx = signed_tx
|
||||||
|
|
||||||
|
|
||||||
def recoverNoScriptTxnWithKey(self, bid_id, encoded_key):
|
def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key):
|
||||||
self.log.info('Manually recovering %s', bid_id.hex())
|
self.log.info('Manually recovering %s', bid_id.hex())
|
||||||
# Manually recover txn if other key is known
|
# Manually recover txn if other key is known
|
||||||
session = scoped_session(self.session_factory)
|
session = scoped_session(self.session_factory)
|
||||||
try:
|
try:
|
||||||
bid, xmr_swap = self.getXmrBidFromSession(session, bid_id)
|
bid, xmr_swap = self.getXmrBidFromSession(session, bid_id)
|
||||||
ensure(bid, 'Bid not found: {}.'.format(bid_id.hex()))
|
ensure(bid, 'Bid not found: {}.'.format(bid_id.hex()))
|
||||||
ensure(xmr_swap, 'XMR swap not found: {}.'.format(bid_id.hex()))
|
ensure(xmr_swap, 'Adaptor-sig swap not found: {}.'.format(bid_id.hex()))
|
||||||
offer, xmr_offer = self.getXmrOfferFromSession(session, bid.offer_id, sent=False)
|
offer, xmr_offer = self.getXmrOfferFromSession(session, bid.offer_id, sent=False)
|
||||||
ensure(offer, 'Offer not found: {}.'.format(bid.offer_id.hex()))
|
ensure(offer, 'Offer not found: {}.'.format(bid.offer_id.hex()))
|
||||||
ensure(xmr_offer, 'XMR offer not found: {}.'.format(bid.offer_id.hex()))
|
ensure(xmr_offer, 'Adaptor-sig offer not found: {}.'.format(bid.offer_id.hex()))
|
||||||
ci_to = self.ci(offer.coin_to)
|
ci_to = self.ci(offer.coin_to)
|
||||||
|
|
||||||
for_ed25519 = True if Coins(offer.coin_to) == Coins.XMR else False
|
for_ed25519 = True if Coins(offer.coin_to) == Coins.XMR else False
|
||||||
@@ -80,7 +86,78 @@ def recoverNoScriptTxnWithKey(self, bid_id, encoded_key):
|
|||||||
|
|
||||||
|
|
||||||
def getChainBSplitKey(swap_client, bid, xmr_swap, offer):
|
def getChainBSplitKey(swap_client, bid, xmr_swap, offer):
|
||||||
ci_to = swap_client.ci(offer.coin_to)
|
reverse_bid: bool = offer.bid_reversed
|
||||||
|
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
|
key_type = KeyTypes.KBSF if bid.was_sent else KeyTypes.KBSL
|
||||||
return ci_to.encodeKey(swap_client.getPathKey(offer.coin_from, offer.coin_to, bid.created_at, xmr_swap.contract_count, key_type, True if offer.coin_to == Coins.XMR 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))
|
||||||
|
|
||||||
|
|
||||||
|
def getChainBRemoteSplitKey(swap_client, bid, xmr_swap, offer):
|
||||||
|
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)
|
||||||
|
|
||||||
|
if bid.was_sent:
|
||||||
|
if xmr_swap.a_lock_refund_spend_tx:
|
||||||
|
af_lock_refund_spend_tx_sig = ci_leader.extractFollowerSig(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)
|
||||||
|
else:
|
||||||
|
if 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)
|
||||||
|
return ci_follower.encodeKey(kbsf)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def setDLEAG(xmr_swap, ci_to, kbsf: bytes) -> None:
|
||||||
|
if ci_to.curve_type() == Curves.ed25519:
|
||||||
|
xmr_swap.kbsf_dleag = ci_to.proveDLEAG(kbsf)
|
||||||
|
xmr_swap.pkasf = xmr_swap.kbsf_dleag[0: 33]
|
||||||
|
elif ci_to.curve_type() == Curves.secp256k1:
|
||||||
|
for i in range(10):
|
||||||
|
xmr_swap.kbsf_dleag = ci_to.signRecoverable(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:
|
||||||
|
break
|
||||||
|
# self.log.debug('kbsl recovered pubkey mismatch, retrying.')
|
||||||
|
assert (pk_recovered == xmr_swap.pkbsf)
|
||||||
|
xmr_swap.pkasf = xmr_swap.pkbsf
|
||||||
|
else:
|
||||||
|
raise ValueError('Unknown curve')
|
||||||
|
|
||||||
|
|
||||||
|
class XmrSwapInterface(ProtocolInterface):
|
||||||
|
swap_type = SwapTypes.XMR_SWAP
|
||||||
|
|
||||||
|
def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes) -> CScript:
|
||||||
|
Kal_enc = Kal if len(Kal) == 33 else ci.encodePubkey(Kal)
|
||||||
|
Kaf_enc = Kaf if len(Kaf) == 33 else ci.encodePubkey(Kaf)
|
||||||
|
|
||||||
|
return CScript([2, Kal_enc, Kaf_enc, 2, CScriptOp(OP_CHECKMULTISIG)])
|
||||||
|
|
||||||
|
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
|
||||||
|
addr_to = self.getMockAddrTo(ci)
|
||||||
|
funded_tx = ci.createRawFundedTransaction(addr_to, amount, sub_fee, lock_unspents=False)
|
||||||
|
|
||||||
|
return bytes.fromhex(funded_tx)
|
||||||
|
|
||||||
|
def promoteMockTx(self, ci, mock_tx: bytes, script: bytearray) -> bytearray:
|
||||||
|
mock_txo_script = self.getMockScriptScriptPubkey(ci)
|
||||||
|
real_txo_script = ci.getScriptDest(script)
|
||||||
|
|
||||||
|
found: int = 0
|
||||||
|
ctx = ci.loadTx(mock_tx)
|
||||||
|
for txo in ctx.vout:
|
||||||
|
if txo.scriptPubKey == mock_txo_script:
|
||||||
|
txo.scriptPubKey = real_txo_script
|
||||||
|
found += 1
|
||||||
|
|
||||||
|
if found < 1:
|
||||||
|
raise ValueError('Mocked output not found')
|
||||||
|
if found > 1:
|
||||||
|
raise ValueError('Too many mocked outputs found')
|
||||||
|
ctx.nLockTime = 0
|
||||||
|
|
||||||
|
return ctx.serialize()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2020-2022 tecnovert
|
# Copyright (c) 2020-2023 tecnovert
|
||||||
# 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.
|
||||||
|
|
||||||
@@ -23,10 +23,7 @@ from .util import jsonDecimal
|
|||||||
def waitForRPC(rpc_func, expect_wallet=True, max_tries=7):
|
def waitForRPC(rpc_func, expect_wallet=True, max_tries=7):
|
||||||
for i in range(max_tries + 1):
|
for i in range(max_tries + 1):
|
||||||
try:
|
try:
|
||||||
if expect_wallet:
|
rpc_func('getwalletinfo' if expect_wallet else 'getblockchaininfo')
|
||||||
rpc_func('getwalletinfo')
|
|
||||||
else:
|
|
||||||
rpc_func('getblockchaininfo')
|
|
||||||
return
|
return
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
if i < max_tries:
|
if i < max_tries:
|
||||||
@@ -111,7 +108,7 @@ def callrpc(rpc_port, auth, method, params=[], wallet=None, host='127.0.0.1'):
|
|||||||
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))
|
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']))
|
||||||
@@ -130,13 +127,15 @@ def openrpc(rpc_port, auth, wallet=None, host='127.0.0.1'):
|
|||||||
raise ValueError('RPC error ' + str(ex))
|
raise ValueError('RPC error ' + str(ex))
|
||||||
|
|
||||||
|
|
||||||
def callrpc_cli(bindir, datadir, chain, cmd, cli_bin='particl-cli'):
|
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 = [cli_bin, ]
|
||||||
if chain != 'mainnet':
|
if chain != 'mainnet':
|
||||||
args.append('-' + chain)
|
args.append('-' + chain)
|
||||||
args.append('-datadir=' + datadir)
|
args.append('-datadir=' + datadir)
|
||||||
|
if wallet is not None:
|
||||||
|
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)
|
||||||
@@ -163,3 +162,9 @@ def make_rpc_func(port, auth, wallet=None, host='127.0.0.1'):
|
|||||||
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:
|
||||||
|
username, password = auth_str.split(':', 1)
|
||||||
|
password = urllib.parse.quote(password, safe='')
|
||||||
|
return f'{username}:{password}'
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
import socks
|
||||||
import time
|
import time
|
||||||
import urllib
|
import urllib
|
||||||
import hashlib
|
import hashlib
|
||||||
@@ -10,9 +11,32 @@ from xmlrpc.client import (
|
|||||||
Transport,
|
Transport,
|
||||||
SafeTransport,
|
SafeTransport,
|
||||||
)
|
)
|
||||||
|
from sockshandler import SocksiPyConnection
|
||||||
from .util import jsonDecimal
|
from .util import jsonDecimal
|
||||||
|
|
||||||
|
|
||||||
|
class SocksTransport(Transport):
|
||||||
|
|
||||||
|
def set_proxy(self, proxy_host, proxy_port):
|
||||||
|
self.proxy_host = proxy_host
|
||||||
|
self.proxy_port = proxy_port
|
||||||
|
|
||||||
|
self.proxy_type = socks.PROXY_TYPE_SOCKS5
|
||||||
|
self.proxy_rdns = True
|
||||||
|
self.proxy_username = None
|
||||||
|
self.proxy_password = None
|
||||||
|
|
||||||
|
def make_connection(self, host):
|
||||||
|
# return an existing connection if possible. This allows
|
||||||
|
# HTTP/1.1 keep-alive.
|
||||||
|
if self._connection and host == self._connection[0]:
|
||||||
|
return self._connection[1]
|
||||||
|
# create a HTTP connection object from a host descriptor
|
||||||
|
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)
|
||||||
|
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__(self, uri, transport=None, encoding=None, verbose=False,
|
||||||
@@ -37,12 +61,15 @@ class JsonrpcDigest():
|
|||||||
self.__verbose = verbose
|
self.__verbose = verbose
|
||||||
self.__allow_none = allow_none
|
self.__allow_none = allow_none
|
||||||
|
|
||||||
self.__request_id = 1
|
self.__request_id = 0
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
if self.__transport is not None:
|
if self.__transport is not None:
|
||||||
self.__transport.close()
|
self.__transport.close()
|
||||||
|
|
||||||
|
def request_id(self):
|
||||||
|
return self.__request_id
|
||||||
|
|
||||||
def post_request(self, method, params, timeout=None):
|
def post_request(self, method, params, timeout=None):
|
||||||
try:
|
try:
|
||||||
connection = self.__transport.make_connection(self.__host)
|
connection = self.__transport.make_connection(self.__host)
|
||||||
@@ -66,7 +93,7 @@ class JsonrpcDigest():
|
|||||||
self.__transport.close()
|
self.__transport.close()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def json_request(self, method, params, 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:
|
||||||
@@ -74,18 +101,11 @@ class JsonrpcDigest():
|
|||||||
|
|
||||||
headers = self.__transport._extra_headers[:]
|
headers = self.__transport._extra_headers[:]
|
||||||
|
|
||||||
request_body = {
|
|
||||||
'method': method,
|
|
||||||
'params': params,
|
|
||||||
'jsonrpc': '2.0',
|
|
||||||
'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(('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'))
|
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:
|
||||||
@@ -135,18 +155,11 @@ class JsonrpcDigest():
|
|||||||
headers = self.__transport._extra_headers[:]
|
headers = self.__transport._extra_headers[:]
|
||||||
headers.append(('Authorization', header_value))
|
headers.append(('Authorization', header_value))
|
||||||
|
|
||||||
request_body = {
|
|
||||||
'method': method,
|
|
||||||
'params': params,
|
|
||||||
'jsonrpc': '2.0',
|
|
||||||
'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(('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'))
|
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
|
||||||
@@ -159,7 +172,7 @@ class JsonrpcDigest():
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def callrpc_xmr(rpc_port, auth, method, params=[], rpc_host='127.0.0.1', path='json_rpc', timeout=120):
|
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:
|
||||||
@@ -167,84 +180,79 @@ def callrpc_xmr(rpc_port, auth, method, params=[], rpc_host='127.0.0.1', path='j
|
|||||||
else:
|
else:
|
||||||
url = 'http://{}:{}/{}'.format(rpc_host, rpc_port, path)
|
url = 'http://{}:{}/{}'.format(rpc_host, rpc_port, path)
|
||||||
|
|
||||||
x = JsonrpcDigest(url)
|
x = JsonrpcDigest(url, transport=transport)
|
||||||
v = x.json_request(method, params, username=auth[0], password=auth[1], timeout=timeout)
|
request_body = {
|
||||||
x.close()
|
'method': method,
|
||||||
r = json.loads(v.decode('utf-8'))
|
'params': params,
|
||||||
except Exception as ex:
|
'jsonrpc': '2.0',
|
||||||
raise ValueError('RPC Server Error: {}'.format(str(ex)))
|
'id': x.request_id()
|
||||||
|
}
|
||||||
if 'error' in r and r['error'] is not None:
|
if auth:
|
||||||
raise ValueError('RPC error ' + str(r['error']))
|
v = x.json_request(request_body, username=auth[0], password=auth[1], timeout=timeout)
|
||||||
|
|
||||||
return r['result']
|
|
||||||
|
|
||||||
|
|
||||||
def callrpc_xmr_na(rpc_port, method, params=[], rpc_host='127.0.0.1', path='json_rpc', timeout=120):
|
|
||||||
try:
|
|
||||||
if rpc_host.count('://') > 0:
|
|
||||||
url = '{}:{}/{}'.format(rpc_host, rpc_port, path)
|
|
||||||
else:
|
else:
|
||||||
url = 'http://{}:{}/{}'.format(rpc_host, rpc_port, path)
|
v = x.json_request(request_body, timeout=timeout)
|
||||||
|
|
||||||
x = JsonrpcDigest(url)
|
|
||||||
v = x.json_request(method, 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(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('RPC error ' + str(r['error']))
|
raise ValueError(tag + 'RPC error ' + str(r['error']))
|
||||||
|
|
||||||
return r['result']
|
return r['result']
|
||||||
|
|
||||||
|
|
||||||
def callrpc_xmr2(rpc_port, method, params=None, rpc_host='127.0.0.1', timeout=120):
|
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)
|
x = JsonrpcDigest(url, transport=transport)
|
||||||
v = x.post_request(method, params, timeout=timeout)
|
if auth:
|
||||||
|
v = x.json_request(params, username=auth[0], password=auth[1], timeout=timeout)
|
||||||
|
else:
|
||||||
|
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(str(ex)))
|
raise ValueError('{}RPC Server Error: {}'.format(tag, str(ex)))
|
||||||
|
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
def make_xmr_rpc_func(port, host='127.0.0.1'):
|
def make_xmr_rpc2_func(port, auth, host='127.0.0.1', proxy_host=None, proxy_port=None, default_timeout=120, tag=''):
|
||||||
port = port
|
|
||||||
host = host
|
|
||||||
|
|
||||||
def rpc_func(method, params=None, wallet=None, timeout=120):
|
|
||||||
nonlocal port
|
|
||||||
nonlocal host
|
|
||||||
return callrpc_xmr_na(port, method, params, rpc_host=host, timeout=timeout)
|
|
||||||
return rpc_func
|
|
||||||
|
|
||||||
|
|
||||||
def make_xmr_rpc2_func(port, host='127.0.0.1'):
|
|
||||||
port = port
|
|
||||||
host = host
|
|
||||||
|
|
||||||
def rpc_func(method, params=None, wallet=None, timeout=120):
|
|
||||||
nonlocal port
|
|
||||||
nonlocal host
|
|
||||||
return callrpc_xmr2(port, method, params, rpc_host=host, timeout=timeout)
|
|
||||||
return rpc_func
|
|
||||||
|
|
||||||
|
|
||||||
def make_xmr_wallet_rpc_func(port, auth, host='127.0.0.1'):
|
|
||||||
port = port
|
port = port
|
||||||
auth = auth
|
auth = auth
|
||||||
host = host
|
host = host
|
||||||
|
transport = None
|
||||||
|
default_timeout = default_timeout
|
||||||
|
tag = tag
|
||||||
|
|
||||||
def rpc_func(method, params=None, wallet=None, timeout=120):
|
if proxy_host:
|
||||||
nonlocal port, auth, host
|
transport = SocksTransport()
|
||||||
return callrpc_xmr(port, auth, method, params, rpc_host=host, timeout=timeout)
|
transport.set_proxy(proxy_host, proxy_port)
|
||||||
|
|
||||||
|
def rpc_func(method, params=None, wallet=None, timeout=default_timeout):
|
||||||
|
nonlocal port, auth, host, transport, tag
|
||||||
|
return callrpc_xmr2(port, method, params, auth=auth, rpc_host=host, timeout=timeout, transport=transport, tag=tag)
|
||||||
|
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=''):
|
||||||
|
port = port
|
||||||
|
auth = auth
|
||||||
|
host = host
|
||||||
|
transport = None
|
||||||
|
default_timeout = default_timeout
|
||||||
|
tag = tag
|
||||||
|
|
||||||
|
if proxy_host:
|
||||||
|
transport = SocksTransport()
|
||||||
|
transport.set_proxy(proxy_host, proxy_port)
|
||||||
|
|
||||||
|
def rpc_func(method, params=None, wallet=None, timeout=default_timeout):
|
||||||
|
nonlocal port, auth, host, transport, tag
|
||||||
|
return callrpc_xmr(port, method, params, rpc_host=host, auth=auth, timeout=timeout, transport=transport, tag=tag)
|
||||||
return rpc_func
|
return rpc_func
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2019 tecnovert
|
# Copyright (c) 2019-2022 tecnovert
|
||||||
# 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.
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@ class OpCodes(IntEnum):
|
|||||||
OP_IF = 0x63,
|
OP_IF = 0x63,
|
||||||
OP_ELSE = 0x67,
|
OP_ELSE = 0x67,
|
||||||
OP_ENDIF = 0x68,
|
OP_ENDIF = 0x68,
|
||||||
|
OP_RETURN = 0x6a,
|
||||||
OP_DROP = 0x75,
|
OP_DROP = 0x75,
|
||||||
OP_DUP = 0x76,
|
OP_DUP = 0x76,
|
||||||
OP_SIZE = 0x82,
|
OP_SIZE = 0x82,
|
||||||
|
|||||||
1
basicswap/static/css/libs/flowbite.min.css
vendored
Normal file
188968
basicswap/static/css/libs/tailwind.min.css
vendored
Normal file
@@ -1,34 +0,0 @@
|
|||||||
|
|
||||||
.padded_row td
|
|
||||||
{
|
|
||||||
padding-top:1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bold
|
|
||||||
{
|
|
||||||
font-weight:bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.monospace
|
|
||||||
{
|
|
||||||
font-family:monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.floatright
|
|
||||||
{
|
|
||||||
position:fixed;
|
|
||||||
top:10px;
|
|
||||||
right:18px;
|
|
||||||
margin: 0;
|
|
||||||
width:calc(33.33% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error
|
|
||||||
{
|
|
||||||
background:#ff5b5b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error_msg
|
|
||||||
{
|
|
||||||
color:red;
|
|
||||||
}
|
|
||||||
141
basicswap/static/css/style.css
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
|
||||||
|
.padded_row td
|
||||||
|
{
|
||||||
|
padding-top:1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bold
|
||||||
|
{
|
||||||
|
font-weight:bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monospace
|
||||||
|
{
|
||||||
|
font-family:monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floatright
|
||||||
|
{
|
||||||
|
position: fixed;
|
||||||
|
top:1.25rem;
|
||||||
|
right:1.25rem;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error_msg
|
||||||
|
{
|
||||||
|
color:red;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hide {
|
||||||
|
-moz-animation: cssAnimation 0s ease-in 15s forwards;
|
||||||
|
/* Firefox */
|
||||||
|
-webkit-animation: cssAnimation 0s ease-in 15s forwards;
|
||||||
|
/* Safari and Chrome */
|
||||||
|
-o-animation: cssAnimation 0s ease-in 15s forwards;
|
||||||
|
/* Opera */
|
||||||
|
animation: cssAnimation 0s ease-in 15s forwards;
|
||||||
|
-webkit-animation-fill-mode: forwards;
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
}
|
||||||
|
@keyframes cssAnimation {
|
||||||
|
to {
|
||||||
|
width:0;
|
||||||
|
height:0;
|
||||||
|
overflow:hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-webkit-keyframes cssAnimation {
|
||||||
|
to {
|
||||||
|
width:0;
|
||||||
|
height:0;
|
||||||
|
visibility:hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select .select {
|
||||||
|
appearance: none;
|
||||||
|
background-image: url('/static/images/other/coin.png');
|
||||||
|
background-position: 10px center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select select::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blurred {
|
||||||
|
filter: blur(4px);
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-overlay.non-blurred {
|
||||||
|
filter: none;
|
||||||
|
pointer-events: auto;
|
||||||
|
user-select: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disable opacity on disabled form elements in Chrome */
|
||||||
|
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||||
|
select:disabled,
|
||||||
|
input:disabled,
|
||||||
|
textarea:disabled {
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add this to your existing CSS file */
|
||||||
|
.error {
|
||||||
|
border: 1px solid red !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-container {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-container::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border: 1px solid rgb(77, 132, 240);
|
||||||
|
border-radius: inherit;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
BIN
basicswap/static/images/coins/Bitcoin-20.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
basicswap/static/images/coins/Bitcoin.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
basicswap/static/images/coins/Dash-20.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
basicswap/static/images/coins/Dash.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
basicswap/static/images/coins/Doge-20.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
basicswap/static/images/coins/Doge.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
basicswap/static/images/coins/Ethereum-20.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
basicswap/static/images/coins/Ethereum.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
basicswap/static/images/coins/Firo-20.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
basicswap/static/images/coins/Firo.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
basicswap/static/images/coins/Litecoin%MWEB.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
basicswap/static/images/coins/Litecoin-20.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
basicswap/static/images/coins/Litecoin-MWEB-20.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
basicswap/static/images/coins/Litecoin-MWEB.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
basicswap/static/images/coins/Litecoin.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
basicswap/static/images/coins/Monero-20.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
basicswap/static/images/coins/Monero.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
basicswap/static/images/coins/PIVX-20.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
basicswap/static/images/coins/PIVX.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
basicswap/static/images/coins/Particl%20Anon.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
basicswap/static/images/coins/Particl%20Blind.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
basicswap/static/images/coins/Particl-20.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
basicswap/static/images/coins/Particl-Anon-20.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
basicswap/static/images/coins/Particl-Anon.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
basicswap/static/images/coins/Particl-Blind-20.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
basicswap/static/images/coins/Particl-Blind.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
basicswap/static/images/coins/Particl.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
basicswap/static/images/coins/ParticlAnon-20.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
basicswap/static/images/coins/ParticlAnon.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
basicswap/static/images/coins/ParticlBlind-20.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
basicswap/static/images/coins/ParticlBlind.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
25
basicswap/static/images/elements/circle-violet.svg
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<svg width="289" height="289" viewBox="0 0 289 289" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<mask id="mask0_1384_22144" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="289" height="289">
|
||||||
|
<circle cx="144.5" cy="144.5" r="142.507" fill="#C4C4C4" stroke="#F8BB54" stroke-width="3.98621"/>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0_1384_22144)">
|
||||||
|
<path d="M-83.9766 60.6005C-83.9766 60.6005 -16.689 48.136 0.672082 4.51091C17.2756 -37.3086 85.6704 -48.5495 85.6704 -48.5495" stroke="#A855F7" stroke-width="4.48062" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-83.9766 81.5681C-83.9766 81.5681 -1.42521 67.182 19.8389 14.529C40.1127 -35.736 123.946 -48.5495 123.946 -48.5495" stroke="#A855F7" stroke-width="4.48062" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-83.9766 102.595C-83.9766 102.595 13.7801 86.2279 38.9475 24.6636C62.9497 -34.2214 162.221 -48.5495 162.221 -48.5495" stroke="#A855F7" stroke-width="4.48062" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-83.9766 123.563C-83.9766 123.563 29.0437 105.274 58.1143 34.6817C85.7869 -32.6488 200.497 -48.5495 200.497 -48.5495" stroke="#A855F7" stroke-width="4.48062" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-83.9766 144.531C-83.9766 144.531 44.3072 124.32 77.2812 44.7578C108.624 -31.0762 238.714 -48.5495 238.714 -48.5495" stroke="#A855F7" stroke-width="4.48062" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-83.9766 165.557C-83.9766 165.557 59.5125 143.424 96.3898 54.8339C131.519 -29.562 276.989 -48.5495 276.989 -48.5495" stroke="#A855F7" stroke-width="4.48062" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-83.9766 186.525C-83.9766 186.525 74.7761 162.411 115.557 64.9105C154.356 -27.9894 315.265 -48.5495 315.265 -48.5495" stroke="#A855F7" stroke-width="4.48062" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-83.9766 207.493C-83.9766 207.493 90.0398 181.457 134.665 74.9286C177.193 -26.4168 353.54 -48.5495 353.54 -48.5495" stroke="#A855F7" stroke-width="4.48062" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-83.9766 228.519C-83.9766 228.519 105.245 200.562 153.832 85.0628C200.031 -24.9026 391.758 -48.5495 391.758 -48.5495" stroke="#A855F7" stroke-width="4.48062" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-83.9766 249.485C-83.9766 249.485 120.509 219.608 172.941 95.1393C222.868 -23.33 430.033 -48.5495 430.033 -48.5495" stroke="#A855F7" stroke-width="4.48062" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-83.9766 270.512C-83.9766 270.512 135.772 238.653 192.108 105.215C245.705 -21.7574 468.308 -48.5495 468.308 -48.5495" stroke="#A855F7" stroke-width="4.48062" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-83.9766 291.482C-83.9766 291.482 150.977 257.7 211.216 115.292C268.542 -20.2428 506.583 -48.5495 506.583 -48.5495" stroke="#A855F7" stroke-width="4.48062" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-83.9766 312.447C-83.9766 312.447 166.241 276.746 230.383 125.31C291.379 -18.6701 544.858 -48.5495 544.858 -48.5495" stroke="#A855F7" stroke-width="4.48062" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-83.9766 333.473C-83.9766 333.473 181.505 295.792 249.55 135.444C314.216 -17.0975 583.076 -48.5495 583.076 -48.5495" stroke="#A855F7" stroke-width="4.48062" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-83.9766 354.443C-83.9766 354.443 196.71 314.837 268.658 145.462C337.053 -15.5833 621.352 -48.5495 621.352 -48.5495" stroke="#A855F7" stroke-width="4.48062" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-83.9766 375.413C-83.9766 375.413 211.974 333.883 287.825 155.538C359.89 -14.0107 659.627 -48.5495 659.627 -48.5495" stroke="#A855F7" stroke-width="4.48062" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-83.9766 396.439C-83.9766 396.439 227.237 352.986 306.934 165.615C382.727 -12.4381 697.902 -48.5495 697.902 -48.5495" stroke="#A855F7" stroke-width="4.48062" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-83.9766 417.405C-83.9766 417.405 242.442 371.975 326.101 175.691C405.565 -10.9239 736.121 -48.5495 736.121 -48.5495" stroke="#A855F7" stroke-width="4.48062" stroke-miterlimit="10"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.7 KiB |
25
basicswap/static/images/elements/circle2-violet.svg
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<svg width="219" height="219" viewBox="0 0 219 219" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<mask id="mask0_1384_30420" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="219" height="219">
|
||||||
|
<circle cx="109.5" cy="109.5" r="107.99" fill="#C4C4C4" stroke="#F8BB54" stroke-width="3.02069"/>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0_1384_30420)">
|
||||||
|
<path d="M-63.6362 45.9223C-63.6362 45.9223 -12.6466 36.4769 0.509327 3.41839C13.0912 -28.2719 64.9198 -36.79 64.9198 -36.79" stroke="#A855F7" stroke-width="3.39535" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-63.6362 61.8112C-63.6362 61.8112 -1.07997 50.9096 15.0337 11.01C30.3968 -27.0802 93.9243 -36.79 93.9243 -36.79" stroke="#A855F7" stroke-width="3.39535" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-63.6362 77.7447C-63.6362 77.7447 10.4424 65.3423 29.5139 18.6898C47.7024 -25.9324 122.929 -36.79 122.929 -36.79" stroke="#A855F7" stroke-width="3.39535" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-63.6362 93.634C-63.6362 93.634 22.0089 79.7751 44.0382 26.2814C65.0081 -24.7407 151.934 -36.79 151.934 -36.79" stroke="#A855F7" stroke-width="3.39535" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-63.6362 109.523C-63.6362 109.523 33.5754 94.2079 58.5626 33.9169C82.3136 -23.549 180.894 -36.79 180.894 -36.79" stroke="#A855F7" stroke-width="3.39535" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-63.6362 125.456C-63.6362 125.456 45.0978 108.685 73.0428 41.5524C99.6633 -22.4016 209.898 -36.79 209.898 -36.79" stroke="#A855F7" stroke-width="3.39535" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-63.6362 141.346C-63.6362 141.346 56.6643 123.073 87.5672 49.1883C116.969 -21.2099 238.903 -36.79 238.903 -36.79" stroke="#A855F7" stroke-width="3.39535" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-63.6362 157.235C-63.6362 157.235 68.2309 137.506 102.047 56.7799C134.275 -20.0182 267.908 -36.79 267.908 -36.79" stroke="#A855F7" stroke-width="3.39535" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-63.6362 173.169C-63.6362 173.169 79.753 151.983 116.572 64.4594C151.58 -18.8707 296.868 -36.79 296.868 -36.79" stroke="#A855F7" stroke-width="3.39535" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-63.6362 189.056C-63.6362 189.056 91.3198 166.416 131.052 72.0953C168.886 -17.679 325.873 -36.79 325.873 -36.79" stroke="#A855F7" stroke-width="3.39535" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-63.6362 204.99C-63.6362 204.99 102.886 180.848 145.576 79.7308C186.192 -16.4873 354.877 -36.79 354.877 -36.79" stroke="#A855F7" stroke-width="3.39535" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-63.6362 220.881C-63.6362 220.881 114.409 195.281 160.057 87.3667C203.497 -15.3396 383.881 -36.79 383.881 -36.79" stroke="#A855F7" stroke-width="3.39535" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-63.6362 236.768C-63.6362 236.768 125.975 209.714 174.581 94.9579C220.803 -14.1479 412.885 -36.79 412.885 -36.79" stroke="#A855F7" stroke-width="3.39535" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-63.6362 252.701C-63.6362 252.701 137.542 224.147 189.105 102.638C238.108 -12.9562 441.847 -36.79 441.847 -36.79" stroke="#A855F7" stroke-width="3.39535" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-63.6362 268.592C-63.6362 268.592 149.064 238.579 203.585 110.229C255.414 -11.8087 470.851 -36.79 470.851 -36.79" stroke="#A855F7" stroke-width="3.39535" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-63.6362 284.483C-63.6362 284.483 160.631 253.012 218.11 117.865C272.72 -10.617 499.856 -36.79 499.856 -36.79" stroke="#A855F7" stroke-width="3.39535" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-63.6362 300.416C-63.6362 300.416 172.197 267.488 232.59 125.501C290.025 -9.42532 528.86 -36.79 528.86 -36.79" stroke="#A855F7" stroke-width="3.39535" stroke-miterlimit="10"/>
|
||||||
|
<path d="M-63.6362 316.303C-63.6362 316.303 183.719 281.878 247.114 133.136C307.331 -8.27792 557.822 -36.79 557.822 -36.79" stroke="#A855F7" stroke-width="3.39535" stroke-miterlimit="10"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.7 KiB |
84
basicswap/static/images/elements/dots-blue.svg
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<svg width="149" height="199" viewBox="0 0 149 199" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_1384_22237)">
|
||||||
|
<path d="M146.249 198.91C147.707 198.91 148.889 197.728 148.889 196.27C148.889 194.812 147.707 193.63 146.249 193.63C144.791 193.63 143.609 194.812 143.609 196.27C143.609 197.728 144.791 198.91 146.249 198.91Z" fill="#3B82F6"/>
|
||||||
|
<path d="M122.31 198.91C123.768 198.91 124.95 197.728 124.95 196.27C124.95 194.812 123.768 193.63 122.31 193.63C120.852 193.63 119.67 194.812 119.67 196.27C119.67 197.728 120.852 198.91 122.31 198.91Z" fill="#3B82F6"/>
|
||||||
|
<path d="M98.3802 198.91C98.9048 198.912 99.4181 198.759 99.8552 198.469C100.292 198.179 100.634 197.765 100.836 197.281C101.038 196.797 101.092 196.264 100.991 195.75C100.889 195.235 100.638 194.762 100.268 194.39C99.8975 194.018 99.4253 193.765 98.911 193.662C98.3967 193.559 97.8633 193.611 97.3785 193.811C96.8937 194.011 96.4793 194.351 96.1876 194.787C95.896 195.223 95.7402 195.736 95.7402 196.26C95.7402 196.961 96.0181 197.634 96.5129 198.131C97.0078 198.627 97.6792 198.908 98.3802 198.91V198.91Z" fill="#3B82F6"/>
|
||||||
|
<path d="M74.4498 198.91C74.9739 198.91 75.4863 198.755 75.9221 198.464C76.3579 198.173 76.6975 197.759 76.8981 197.274C77.0986 196.79 77.1511 196.257 77.0488 195.743C76.9466 195.229 76.6942 194.757 76.3236 194.387C75.953 194.016 75.4808 193.764 74.9668 193.661C74.4527 193.559 73.9199 193.612 73.4357 193.812C72.9514 194.013 72.5376 194.352 72.2465 194.788C71.9553 195.224 71.7998 195.736 71.7998 196.26C71.7998 196.608 71.8683 196.953 72.0015 197.274C72.1346 197.596 72.3299 197.888 72.5759 198.134C72.822 198.38 73.1142 198.576 73.4357 198.709C73.7572 198.842 74.1018 198.91 74.4498 198.91V198.91Z" fill="#3B82F6"/>
|
||||||
|
<path d="M50.5101 198.91C51.9681 198.91 53.1501 197.728 53.1501 196.27C53.1501 194.812 51.9681 193.63 50.5101 193.63C49.0521 193.63 47.8701 194.812 47.8701 196.27C47.8701 197.728 49.0521 198.91 50.5101 198.91Z" fill="#3B82F6"/>
|
||||||
|
<path d="M27.5901 198.595C28.9386 198.041 29.5823 196.498 29.0279 195.15C28.4735 193.801 26.9308 193.157 25.5823 193.712C24.2338 194.266 23.5901 195.809 24.1445 197.157C24.6989 198.506 26.2416 199.15 27.5901 198.595Z" fill="#3B82F6"/>
|
||||||
|
<path d="M2.65017 193.66C2.1252 193.658 1.61153 193.812 1.17421 194.103C0.736895 194.393 0.395678 194.807 0.193868 195.291C-0.00794293 195.776 -0.0612624 196.31 0.0406694 196.825C0.142601 197.34 0.395167 197.813 0.766377 198.184C1.13759 198.555 1.61065 198.808 2.12563 198.91C2.64061 199.012 3.17433 198.958 3.65896 198.756C4.14359 198.555 4.55737 198.214 4.8478 197.776C5.13822 197.339 5.29216 196.825 5.29018 196.3C5.28755 195.601 5.00858 194.931 4.51406 194.436C4.01953 193.942 3.34953 193.663 2.65017 193.66V193.66Z" fill="#3B82F6"/>
|
||||||
|
<path d="M146.249 177.5C147.707 177.5 148.889 176.318 148.889 174.86C148.889 173.402 147.707 172.22 146.249 172.22C144.791 172.22 143.609 173.402 143.609 174.86C143.609 176.318 144.791 177.5 146.249 177.5Z" fill="#3B82F6"/>
|
||||||
|
<path d="M122.31 177.5C123.768 177.5 124.95 176.318 124.95 174.86C124.95 173.402 123.768 172.22 122.31 172.22C120.852 172.22 119.67 173.402 119.67 174.86C119.67 176.318 120.852 177.5 122.31 177.5Z" fill="#3B82F6"/>
|
||||||
|
<path d="M98.3803 177.5C98.9052 177.502 99.419 177.348 99.8563 177.058C100.294 176.767 100.635 176.354 100.837 175.869C101.038 175.384 101.092 174.851 100.99 174.336C100.888 173.821 100.635 173.348 100.264 172.976C99.893 172.605 99.4198 172.353 98.9048 172.251C98.3898 172.149 97.8562 172.202 97.3716 172.404C96.887 172.606 96.4732 172.947 96.1828 173.384C95.8923 173.821 95.7383 174.335 95.7403 174.86C95.7429 175.56 96.022 176.229 96.5165 176.724C97.011 177.219 97.6809 177.498 98.3803 177.5V177.5Z" fill="#3B82F6"/>
|
||||||
|
<path d="M74.4498 177.5C74.9744 177.5 75.4871 177.345 75.9231 177.053C76.3591 176.761 76.6988 176.347 76.8991 175.862C77.0993 175.377 77.1512 174.844 77.0481 174.329C76.9451 173.815 76.6917 173.343 76.3201 172.973C75.9485 172.603 75.4753 172.351 74.9606 172.25C74.4459 172.149 73.9128 172.203 73.4287 172.405C72.9447 172.607 72.5316 172.948 72.2416 173.385C71.9516 173.822 71.7978 174.336 71.7998 174.86C71.7998 175.208 71.8684 175.552 72.0017 175.873C72.135 176.194 72.3304 176.485 72.5766 176.73C72.8228 176.976 73.115 177.17 73.4364 177.302C73.7579 177.434 74.1023 177.502 74.4498 177.5V177.5Z" fill="#3B82F6"/>
|
||||||
|
<path d="M50.5101 177.5C51.9681 177.5 53.1501 176.318 53.1501 174.86C53.1501 173.402 51.9681 172.22 50.5101 172.22C49.0521 172.22 47.8701 173.402 47.8701 174.86C47.8701 176.318 49.0521 177.5 50.5101 177.5Z" fill="#3B82F6"/>
|
||||||
|
<path d="M27.5921 177.193C28.9406 176.639 29.5843 175.096 29.0299 173.747C28.4754 172.399 26.9328 171.755 25.5843 172.31C24.2358 172.864 23.592 174.407 24.1465 175.755C24.7009 177.104 26.2436 177.747 27.5921 177.193Z" fill="#3B82F6"/>
|
||||||
|
<path d="M2.65004 177.5C3.17459 177.5 3.68731 177.345 4.12331 177.053C4.55931 176.761 4.89899 176.347 5.09926 175.862C5.29954 175.377 5.3515 174.844 5.24843 174.329C5.14537 173.815 4.89189 173.343 4.52028 172.973C4.14867 172.603 3.67561 172.351 3.16091 172.25C2.6462 172.149 2.11309 172.203 1.62905 172.405C1.145 172.607 0.731771 172.948 0.441791 173.385C0.151812 173.822 -0.00196055 174.336 1.88721e-05 174.86C1.63788e-05 175.208 0.0686259 175.552 0.201923 175.873C0.33522 176.194 0.530667 176.485 0.776874 176.73C1.02308 176.976 1.31528 177.17 1.63674 177.302C1.95819 177.434 2.30251 177.502 2.65004 177.5Z" fill="#3B82F6"/>
|
||||||
|
<path d="M146.249 156.09C147.707 156.09 148.889 154.908 148.889 153.45C148.889 151.992 147.707 150.81 146.249 150.81C144.791 150.81 143.609 151.992 143.609 153.45C143.609 154.908 144.791 156.09 146.249 156.09Z" fill="#3B82F6"/>
|
||||||
|
<path d="M122.31 156.09C123.768 156.09 124.95 154.908 124.95 153.45C124.95 151.992 123.768 150.81 122.31 150.81C120.852 150.81 119.67 151.992 119.67 153.45C119.67 154.908 120.852 156.09 122.31 156.09Z" fill="#3B82F6"/>
|
||||||
|
<path d="M98.3802 156.1C98.9048 156.102 99.4181 155.948 99.8552 155.658C100.292 155.368 100.634 154.955 100.836 154.471C101.038 153.987 101.092 153.454 100.991 152.939C100.889 152.424 100.638 151.951 100.268 151.58C99.8975 151.208 99.4253 150.955 98.911 150.851C98.3967 150.748 97.8633 150.8 97.3785 151.001C96.8937 151.201 96.4793 151.541 96.1876 151.977C95.896 152.413 95.7402 152.925 95.7402 153.45C95.7402 154.151 96.0181 154.823 96.5129 155.32C97.0078 155.817 97.6792 156.097 98.3802 156.1V156.1Z" fill="#3B82F6"/>
|
||||||
|
<path d="M74.4498 156.1C74.9739 156.1 75.4863 155.944 75.9221 155.653C76.3579 155.362 76.6975 154.948 76.8981 154.464C77.0986 153.98 77.1511 153.447 77.0488 152.933C76.9466 152.419 76.6942 151.947 76.3236 151.576C75.953 151.205 75.4808 150.953 74.9668 150.851C74.4527 150.748 73.9199 150.801 73.4357 151.002C72.9514 151.202 72.5376 151.542 72.2465 151.978C71.9553 152.413 71.7998 152.926 71.7998 153.45C71.7998 153.798 71.8683 154.142 72.0015 154.464C72.1346 154.785 72.3299 155.078 72.5759 155.324C72.822 155.57 73.1142 155.765 73.4357 155.898C73.7572 156.031 74.1018 156.1 74.4498 156.1V156.1Z" fill="#3B82F6"/>
|
||||||
|
<path d="M50.5101 156.09C51.9681 156.09 53.1501 154.908 53.1501 153.45C53.1501 151.992 51.9681 150.81 50.5101 150.81C49.0521 150.81 47.8701 151.992 47.8701 153.45C47.8701 154.908 49.0521 156.09 50.5101 156.09Z" fill="#3B82F6"/>
|
||||||
|
<path d="M27.5911 155.791C28.9396 155.237 29.5833 153.694 29.0289 152.346C28.4744 150.997 26.9318 150.354 25.5833 150.908C24.2348 151.463 23.591 153.005 24.1455 154.354C24.6999 155.702 26.2426 156.346 27.5911 155.791Z" fill="#3B82F6"/>
|
||||||
|
<path d="M2.65002 156.1C3.17414 156.1 3.68652 155.944 4.12231 155.653C4.5581 155.362 4.89769 154.948 5.09827 154.464C5.29884 153.98 5.3514 153.447 5.24915 152.933C5.14689 152.419 4.89453 151.947 4.52393 151.576C4.15332 151.205 3.68104 150.953 3.16699 150.851C2.65294 150.748 2.12021 150.801 1.63599 151.002C1.15176 151.202 0.737841 151.542 0.446655 151.978C0.15547 152.413 0 152.926 0 153.45C0 154.153 0.279274 154.827 0.776245 155.324C1.27322 155.821 1.9472 156.1 2.65002 156.1V156.1Z" fill="#3B82F6"/>
|
||||||
|
<path d="M146.249 134.69C147.707 134.69 148.889 133.508 148.889 132.05C148.889 130.592 147.707 129.41 146.249 129.41C144.791 129.41 143.609 130.592 143.609 132.05C143.609 133.508 144.791 134.69 146.249 134.69Z" fill="#3B82F6"/>
|
||||||
|
<path d="M122.31 134.69C123.768 134.69 124.95 133.508 124.95 132.05C124.95 130.592 123.768 129.41 122.31 129.41C120.852 129.41 119.67 130.592 119.67 132.05C119.67 133.508 120.852 134.69 122.31 134.69Z" fill="#3B82F6"/>
|
||||||
|
<path d="M98.3803 134.66C98.9052 134.662 99.419 134.508 99.8563 134.218C100.294 133.927 100.635 133.514 100.837 133.029C101.038 132.544 101.092 132.011 100.99 131.496C100.888 130.981 100.635 130.508 100.264 130.137C99.893 129.765 99.4198 129.513 98.9048 129.411C98.3898 129.309 97.8562 129.362 97.3716 129.564C96.887 129.766 96.4732 130.107 96.1828 130.544C95.8923 130.982 95.7383 131.495 95.7403 132.02C95.7429 132.72 96.022 133.39 96.5165 133.884C97.011 134.379 97.6809 134.658 98.3803 134.66V134.66Z" fill="#3B82F6"/>
|
||||||
|
<path d="M74.4498 134.66C74.9744 134.66 75.4871 134.505 75.9231 134.213C76.3591 133.921 76.6988 133.507 76.8991 133.022C77.0993 132.537 77.1512 132.004 77.0481 131.49C76.9451 130.975 76.6917 130.503 76.3201 130.133C75.9485 129.763 75.4753 129.511 74.9606 129.41C74.4459 129.309 73.9128 129.363 73.4287 129.565C72.9447 129.767 72.5316 130.108 72.2416 130.545C71.9516 130.982 71.7978 131.496 71.7998 132.02C71.7998 132.368 71.8684 132.712 72.0017 133.033C72.135 133.354 72.3304 133.645 72.5766 133.891C72.8228 134.136 73.115 134.33 73.4364 134.462C73.7579 134.594 74.1023 134.662 74.4498 134.66V134.66Z" fill="#3B82F6"/>
|
||||||
|
<path d="M50.5101 134.69C51.9681 134.69 53.1501 133.508 53.1501 132.05C53.1501 130.592 51.9681 129.41 50.5101 129.41C49.0521 129.41 47.8701 130.592 47.8701 132.05C47.8701 133.508 49.0521 134.69 50.5101 134.69Z" fill="#3B82F6"/>
|
||||||
|
<path d="M27.594 134.389C28.9425 133.835 29.5863 132.292 29.0318 130.944C28.4774 129.595 26.9347 128.951 25.5862 129.506C24.2377 130.06 23.594 131.603 24.1484 132.951C24.7029 134.3 26.2455 134.944 27.594 134.389Z" fill="#3B82F6"/>
|
||||||
|
<path d="M2.65004 134.66C3.17459 134.66 3.68731 134.505 4.12331 134.213C4.55931 133.921 4.89899 133.507 5.09926 133.022C5.29954 132.537 5.3515 132.004 5.24843 131.49C5.14537 130.975 4.89189 130.503 4.52028 130.133C4.14867 129.763 3.67561 129.511 3.16091 129.41C2.6462 129.309 2.11309 129.363 1.62905 129.565C1.145 129.767 0.731771 130.108 0.441791 130.545C0.151812 130.982 -0.00196055 131.496 1.88721e-05 132.02C1.63788e-05 132.368 0.0686259 132.712 0.201923 133.033C0.33522 133.354 0.530667 133.645 0.776874 133.891C1.02308 134.136 1.31528 134.33 1.63674 134.462C1.95819 134.594 2.30251 134.662 2.65004 134.66V134.66Z" fill="#3B82F6"/>
|
||||||
|
<path d="M146.249 113.28C147.707 113.28 148.889 112.098 148.889 110.64C148.889 109.182 147.707 108 146.249 108C144.791 108 143.609 109.182 143.609 110.64C143.609 112.098 144.791 113.28 146.249 113.28Z" fill="#3B82F6"/>
|
||||||
|
<path d="M122.31 113.28C123.768 113.28 124.95 112.098 124.95 110.64C124.95 109.182 123.768 108 122.31 108C120.852 108 119.67 109.182 119.67 110.64C119.67 112.098 120.852 113.28 122.31 113.28Z" fill="#3B82F6"/>
|
||||||
|
<path d="M98.3802 113.29C98.9048 113.292 99.4181 113.139 99.8552 112.849C100.292 112.559 100.634 112.145 100.836 111.661C101.038 111.177 101.092 110.644 100.991 110.129C100.889 109.615 100.638 109.142 100.268 108.77C99.8975 108.398 99.4253 108.145 98.911 108.042C98.3967 107.939 97.8633 107.991 97.3785 108.191C96.8937 108.391 96.4793 108.731 96.1876 109.167C95.896 109.603 95.7402 110.116 95.7402 110.64C95.7402 111.341 96.0181 112.014 96.5129 112.511C97.0078 113.007 97.6792 113.288 98.3802 113.29V113.29Z" fill="#3B82F6"/>
|
||||||
|
<path d="M74.4498 113.29C74.9739 113.29 75.4863 113.135 75.9221 112.844C76.3579 112.552 76.6975 112.139 76.8981 111.654C77.0986 111.17 77.1511 110.637 77.0488 110.123C76.9466 109.609 76.6942 109.137 76.3236 108.766C75.953 108.396 75.4808 108.143 74.9668 108.041C74.4527 107.939 73.9199 107.991 73.4357 108.192C72.9514 108.393 72.5376 108.732 72.2465 109.168C71.9553 109.604 71.7998 110.116 71.7998 110.64C71.7998 110.988 71.8683 111.333 72.0015 111.654C72.1346 111.976 72.3299 112.268 72.5759 112.514C72.822 112.76 73.1142 112.955 73.4357 113.089C73.7572 113.222 74.1018 113.29 74.4498 113.29V113.29Z" fill="#3B82F6"/>
|
||||||
|
<path d="M50.5101 113.28C51.9681 113.28 53.1501 112.098 53.1501 110.64C53.1501 109.182 51.9681 108 50.5101 108C49.0521 108 47.8701 109.182 47.8701 110.64C47.8701 112.098 49.0521 113.28 50.5101 113.28Z" fill="#3B82F6"/>
|
||||||
|
<path d="M27.592 112.977C28.9406 112.423 29.5843 110.88 29.0298 109.531C28.4754 108.183 26.9328 107.539 25.5843 108.094C24.2358 108.648 23.592 110.191 24.1465 111.539C24.7009 112.888 26.2435 113.532 27.592 112.977Z" fill="#3B82F6"/>
|
||||||
|
<path d="M2.65002 113.29C3.17414 113.29 3.68652 113.135 4.12231 112.844C4.5581 112.552 4.89769 112.139 5.09827 111.654C5.29884 111.17 5.3514 110.637 5.24915 110.123C5.14689 109.609 4.89453 109.137 4.52393 108.766C4.15332 108.396 3.68104 108.143 3.16699 108.041C2.65294 107.939 2.12021 107.991 1.63599 108.192C1.15176 108.393 0.737841 108.732 0.446655 109.168C0.15547 109.604 0 110.116 0 110.64C0 111.343 0.279274 112.017 0.776245 112.514C1.27322 113.011 1.9472 113.29 2.65002 113.29V113.29Z" fill="#3B82F6"/>
|
||||||
|
</g>
|
||||||
|
<g clip-path="url(#clip1_1384_22237)">
|
||||||
|
<path d="M146.249 90.9099C147.707 90.9099 148.889 89.7279 148.889 88.2699C148.889 86.8119 147.707 85.6299 146.249 85.6299C144.791 85.6299 143.609 86.8119 143.609 88.2699C143.609 89.7279 144.791 90.9099 146.249 90.9099Z" fill="#3B82F6"/>
|
||||||
|
<path d="M122.31 90.9099C123.768 90.9099 124.95 89.7279 124.95 88.2699C124.95 86.8119 123.768 85.6299 122.31 85.6299C120.852 85.6299 119.67 86.8119 119.67 88.2699C119.67 89.7279 120.852 90.9099 122.31 90.9099Z" fill="#3B82F6"/>
|
||||||
|
<path d="M98.3802 90.9104C98.9048 90.9123 99.4181 90.7586 99.8552 90.4686C100.292 90.1787 100.634 89.7654 100.836 89.2814C101.038 88.7973 101.092 88.2642 100.991 87.7495C100.889 87.2348 100.638 86.7617 100.268 86.3901C99.8975 86.0185 99.4253 85.7651 98.911 85.662C98.3967 85.5589 97.8633 85.6108 97.3785 85.8111C96.8937 86.0114 96.4793 86.3511 96.1876 86.7871C95.896 87.2231 95.7402 87.7358 95.7402 88.2603C95.7402 88.9614 96.0181 89.6339 96.5129 90.1306C97.0078 90.6273 97.6792 90.9077 98.3802 90.9104V90.9104Z" fill="#3B82F6"/>
|
||||||
|
<path d="M74.4498 90.9104C74.9739 90.9104 75.4863 90.7549 75.9221 90.4638C76.3579 90.1726 76.6975 89.7587 76.8981 89.2745C77.0986 88.7903 77.1511 88.2574 77.0488 87.7434C76.9466 87.2293 76.6942 86.7572 76.3236 86.3865C75.953 86.0159 75.4808 85.7635 74.9668 85.6613C74.4527 85.559 73.9199 85.6115 73.4357 85.8121C72.9514 86.0127 72.5376 86.3523 72.2465 86.7881C71.9553 87.2239 71.7998 87.7362 71.7998 88.2603C71.7998 88.6083 71.8683 88.953 72.0015 89.2745C72.1346 89.596 72.3299 89.8881 72.5759 90.1342C72.822 90.3802 73.1142 90.5755 73.4357 90.7087C73.7572 90.8419 74.1018 90.9104 74.4498 90.9104V90.9104Z" fill="#3B82F6"/>
|
||||||
|
<path d="M50.5101 90.9099C51.9681 90.9099 53.1501 89.7279 53.1501 88.2699C53.1501 86.8119 51.9681 85.6299 50.5101 85.6299C49.0521 85.6299 47.8701 86.8119 47.8701 88.2699C47.8701 89.7279 49.0521 90.9099 50.5101 90.9099Z" fill="#3B82F6"/>
|
||||||
|
<path d="M27.5901 90.5952C28.9386 90.0408 29.5823 88.4982 29.0279 87.1497C28.4735 85.8012 26.9308 85.1575 25.5823 85.7119C24.2338 86.2664 23.5901 87.809 24.1445 89.1575C24.6989 90.506 26.2416 91.1497 27.5901 90.5952Z" fill="#3B82F6"/>
|
||||||
|
<path d="M2.65017 85.6602C2.1252 85.6582 1.61153 85.8122 1.17421 86.1026C0.736895 86.393 0.395678 86.8068 0.193868 87.2915C-0.00794293 87.7761 -0.0612624 88.3097 0.0406694 88.8247C0.142601 89.3397 0.395167 89.8128 0.766377 90.184C1.13759 90.5552 1.61065 90.8078 2.12563 90.9097C2.64061 91.0116 3.17433 90.9583 3.65896 90.7565C4.14359 90.5547 4.55737 90.2135 4.8478 89.7762C5.13822 89.3389 5.29216 88.8252 5.29018 88.3002C5.28755 87.6008 5.00858 86.9308 4.51406 86.4363C4.01953 85.9418 3.34953 85.6628 2.65017 85.6602V85.6602Z" fill="#3B82F6"/>
|
||||||
|
<path d="M146.249 69.4997C147.707 69.4997 148.889 68.3178 148.889 66.8597C148.889 65.4017 147.707 64.2197 146.249 64.2197C144.791 64.2197 143.609 65.4017 143.609 66.8597C143.609 68.3178 144.791 69.4997 146.249 69.4997Z" fill="#3B82F6"/>
|
||||||
|
<path d="M122.31 69.4997C123.768 69.4997 124.95 68.3178 124.95 66.8597C124.95 65.4017 123.768 64.2197 122.31 64.2197C120.852 64.2197 119.67 65.4017 119.67 66.8597C119.67 68.3178 120.852 69.4997 122.31 69.4997Z" fill="#3B82F6"/>
|
||||||
|
<path d="M98.3803 69.5002C98.9052 69.5022 99.419 69.3482 99.8563 69.0578C100.294 68.7673 100.635 68.3535 100.837 67.8689C101.038 67.3843 101.092 66.8506 100.99 66.3357C100.888 65.8207 100.635 65.3475 100.264 64.9763C99.893 64.6051 99.4198 64.3526 98.9048 64.2506C98.3898 64.1487 97.8562 64.202 97.3716 64.4038C96.887 64.6056 96.4732 64.9469 96.1828 65.3842C95.8923 65.8215 95.7383 66.3352 95.7403 66.8602C95.7429 67.5595 96.022 68.2295 96.5165 68.724C97.011 69.2185 97.6809 69.4976 98.3803 69.5002V69.5002Z" fill="#3B82F6"/>
|
||||||
|
<path d="M74.4498 69.5002C74.9744 69.5002 75.4871 69.3445 75.9231 69.0529C76.3591 68.7612 76.6988 68.3468 76.8991 67.862C77.0993 67.3772 77.1512 66.8438 77.0481 66.3295C76.9451 65.8152 76.6917 65.343 76.3201 64.9728C75.9485 64.6026 75.4753 64.351 74.9606 64.2499C74.4459 64.1488 73.9128 64.2027 73.4287 64.4048C72.9447 64.6069 72.5316 64.9481 72.2416 65.3852C71.9516 65.8223 71.7978 66.3356 71.7998 66.8602C71.7998 67.2077 71.8684 67.5518 72.0017 67.8728C72.135 68.1937 72.3304 68.4852 72.5766 68.7305C72.8228 68.9758 73.115 69.17 73.4364 69.3021C73.7579 69.4342 74.1023 69.5015 74.4498 69.5002V69.5002Z" fill="#3B82F6"/>
|
||||||
|
<path d="M50.5101 69.4997C51.9681 69.4997 53.1501 68.3178 53.1501 66.8597C53.1501 65.4017 51.9681 64.2197 50.5101 64.2197C49.0521 64.2197 47.8701 65.4017 47.8701 66.8597C47.8701 68.3178 49.0521 69.4997 50.5101 69.4997Z" fill="#3B82F6"/>
|
||||||
|
<path d="M27.5921 69.193C28.9406 68.6385 29.5843 67.0959 29.0299 65.7474C28.4754 64.3989 26.9328 63.7551 25.5843 64.3096C24.2358 64.864 23.592 66.4067 24.1465 67.7552C24.7009 69.1037 26.2436 69.7474 27.5921 69.193Z" fill="#3B82F6"/>
|
||||||
|
<path d="M2.65004 69.5002C3.17459 69.5002 3.68731 69.3445 4.12331 69.0529C4.55931 68.7612 4.89899 68.3468 5.09926 67.862C5.29954 67.3772 5.3515 66.8438 5.24843 66.3295C5.14537 65.8152 4.89189 65.343 4.52028 64.9728C4.14867 64.6026 3.67561 64.351 3.16091 64.2499C2.6462 64.1488 2.11309 64.2027 1.62905 64.4048C1.145 64.6069 0.731771 64.9481 0.441791 65.3852C0.151812 65.8223 -0.00196055 66.3356 1.88721e-05 66.8602C1.63788e-05 67.2077 0.0686259 67.5518 0.201923 67.8728C0.33522 68.1937 0.530667 68.4852 0.776874 68.7305C1.02308 68.9758 1.31528 69.17 1.63674 69.3021C1.95819 69.4342 2.30251 69.5015 2.65004 69.5002Z" fill="#3B82F6"/>
|
||||||
|
<path d="M146.249 48.0896C147.707 48.0896 148.889 46.9076 148.889 45.4496C148.889 43.9915 147.707 42.8096 146.249 42.8096C144.791 42.8096 143.609 43.9915 143.609 45.4496C143.609 46.9076 144.791 48.0896 146.249 48.0896Z" fill="#3B82F6"/>
|
||||||
|
<path d="M122.31 48.0896C123.768 48.0896 124.95 46.9076 124.95 45.4496C124.95 43.9915 123.768 42.8096 122.31 42.8096C120.852 42.8096 119.67 43.9915 119.67 45.4496C119.67 46.9076 120.852 48.0896 122.31 48.0896Z" fill="#3B82F6"/>
|
||||||
|
<path d="M98.3802 48.0998C98.9048 48.1018 99.4181 47.9481 99.8552 47.6581C100.292 47.3681 100.634 46.9549 100.836 46.4708C101.038 45.9868 101.092 45.4537 100.991 44.939C100.889 44.4243 100.638 43.9512 100.268 43.5795C99.8975 43.2079 99.4253 42.9545 98.911 42.8515C98.3967 42.7484 97.8633 42.8003 97.3785 43.0006C96.8937 43.2008 96.4793 43.5405 96.1876 43.9765C95.896 44.4125 95.7402 44.9252 95.7402 45.4498C95.7402 46.1509 96.0181 46.8234 96.5129 47.3201C97.0078 47.8168 97.6792 48.0972 98.3802 48.0998V48.0998Z" fill="#3B82F6"/>
|
||||||
|
<path d="M74.4498 48.0998C74.9739 48.0998 75.4863 47.9444 75.9221 47.6532C76.3579 47.362 76.6975 46.9482 76.8981 46.4639C77.0986 45.9797 77.1511 45.4469 77.0488 44.9328C76.9466 44.4188 76.6942 43.9465 76.3236 43.5759C75.953 43.2053 75.4808 42.953 74.9668 42.8507C74.4527 42.7485 73.9199 42.801 73.4357 43.0015C72.9514 43.2021 72.5376 43.5418 72.2465 43.9776C71.9553 44.4133 71.7998 44.9257 71.7998 45.4498C71.7998 45.7978 71.8683 46.1424 72.0015 46.4639C72.1346 46.7855 72.3299 47.0775 72.5759 47.3236C72.822 47.5697 73.1142 47.7649 73.4357 47.8981C73.7572 48.0313 74.1018 48.0998 74.4498 48.0998V48.0998Z" fill="#3B82F6"/>
|
||||||
|
<path d="M50.5101 48.0896C51.9681 48.0896 53.1501 46.9076 53.1501 45.4496C53.1501 43.9915 51.9681 42.8096 50.5101 42.8096C49.0521 42.8096 47.8701 43.9915 47.8701 45.4496C47.8701 46.9076 49.0521 48.0896 50.5101 48.0896Z" fill="#3B82F6"/>
|
||||||
|
<path d="M27.5911 47.7915C28.9396 47.237 29.5833 45.6944 29.0289 44.3459C28.4744 42.9974 26.9318 42.3537 25.5833 42.9081C24.2348 43.4626 23.591 45.0052 24.1455 46.3537C24.6999 47.7022 26.2426 48.3459 27.5911 47.7915Z" fill="#3B82F6"/>
|
||||||
|
<path d="M2.65002 48.0998C3.17414 48.0998 3.68652 47.9444 4.12231 47.6532C4.5581 47.362 4.89769 46.9482 5.09827 46.4639C5.29884 45.9797 5.3514 45.4469 5.24915 44.9328C5.14689 44.4188 4.89453 43.9465 4.52393 43.5759C4.15332 43.2053 3.68104 42.953 3.16699 42.8507C2.65294 42.7485 2.12021 42.801 1.63599 43.0015C1.15176 43.2021 0.737841 43.5418 0.446655 43.9776C0.15547 44.4133 0 44.9257 0 45.4498C0 46.1526 0.279274 46.8266 0.776245 47.3236C1.27322 47.8206 1.9472 48.0998 2.65002 48.0998V48.0998Z" fill="#3B82F6"/>
|
||||||
|
<path d="M146.249 26.6902C147.707 26.6902 148.889 25.5082 148.889 24.0502C148.889 22.5921 147.707 21.4102 146.249 21.4102C144.791 21.4102 143.609 22.5921 143.609 24.0502C143.609 25.5082 144.791 26.6902 146.249 26.6902Z" fill="#3B82F6"/>
|
||||||
|
<path d="M122.31 26.6902C123.768 26.6902 124.95 25.5082 124.95 24.0502C124.95 22.5921 123.768 21.4102 122.31 21.4102C120.852 21.4102 119.67 22.5921 119.67 24.0502C119.67 25.5082 120.852 26.6902 122.31 26.6902Z" fill="#3B82F6"/>
|
||||||
|
<path d="M98.3803 26.6604C98.9052 26.6623 99.419 26.5083 99.8563 26.2179C100.294 25.9275 100.635 25.5137 100.837 25.0291C101.038 24.5444 101.092 24.0108 100.99 23.4958C100.888 22.9808 100.635 22.5078 100.264 22.1366C99.893 21.7653 99.4198 21.5127 98.9048 21.4108C98.3898 21.3089 97.8562 21.3622 97.3716 21.564C96.887 21.7658 96.4732 22.107 96.1828 22.5443C95.8923 22.9817 95.7383 23.4954 95.7403 24.0203C95.7429 24.7197 96.022 25.3897 96.5165 25.8842C97.011 26.3788 97.6809 26.6577 98.3803 26.6604V26.6604Z" fill="#3B82F6"/>
|
||||||
|
<path d="M74.4498 26.6604C74.9744 26.6604 75.4871 26.5047 75.9231 26.213C76.3591 25.9214 76.6988 25.507 76.8991 25.0222C77.0993 24.5374 77.1512 24.004 77.0481 23.4897C76.9451 22.9754 76.6917 22.5032 76.3201 22.133C75.9485 21.7628 75.4753 21.5112 74.9606 21.4101C74.4459 21.3089 73.9128 21.3629 73.4287 21.565C72.9447 21.7671 72.5316 22.1083 72.2416 22.5454C71.9516 22.9825 71.7978 23.4958 71.7998 24.0203C71.7998 24.3679 71.8684 24.712 72.0017 25.033C72.135 25.3539 72.3304 25.6454 72.5766 25.8906C72.8228 26.1359 73.115 26.3302 73.4364 26.4623C73.7579 26.5944 74.1023 26.6617 74.4498 26.6604V26.6604Z" fill="#3B82F6"/>
|
||||||
|
<path d="M50.5101 26.6902C51.9681 26.6902 53.1501 25.5082 53.1501 24.0502C53.1501 22.5921 51.9681 21.4102 50.5101 21.4102C49.0521 21.4102 47.8701 22.5921 47.8701 24.0502C47.8701 25.5082 49.0521 26.6902 50.5101 26.6902Z" fill="#3B82F6"/>
|
||||||
|
<path d="M27.594 26.3892C28.9425 25.8348 29.5863 24.2922 29.0318 22.9437C28.4774 21.5952 26.9347 20.9514 25.5862 21.5059C24.2377 22.0603 23.594 23.603 24.1484 24.9515C24.7029 26.3 26.2455 26.9437 27.594 26.3892Z" fill="#3B82F6"/>
|
||||||
|
<path d="M2.65004 26.6604C3.17459 26.6604 3.68731 26.5047 4.12331 26.213C4.55931 25.9214 4.89899 25.507 5.09926 25.0222C5.29954 24.5374 5.3515 24.004 5.24843 23.4897C5.14537 22.9754 4.89189 22.5032 4.52028 22.133C4.14867 21.7628 3.67561 21.5112 3.16091 21.4101C2.6462 21.3089 2.11309 21.3629 1.62905 21.565C1.145 21.7671 0.731771 22.1083 0.441791 22.5454C0.151812 22.9825 -0.00196055 23.4958 1.88721e-05 24.0203C1.63788e-05 24.3679 0.0686259 24.712 0.201923 25.033C0.33522 25.3539 0.530667 25.6454 0.776874 25.8906C1.02308 26.1359 1.31528 26.3302 1.63674 26.4623C1.95819 26.5944 2.30251 26.6617 2.65004 26.6604V26.6604Z" fill="#3B82F6"/>
|
||||||
|
<path d="M146.249 5.28C147.707 5.28 148.889 4.09803 148.889 2.64C148.889 1.18197 147.707 0 146.249 0C144.791 0 143.609 1.18197 143.609 2.64C143.609 4.09803 144.791 5.28 146.249 5.28Z" fill="#3B82F6"/>
|
||||||
|
<path d="M122.31 5.28C123.768 5.28 124.95 4.09803 124.95 2.64C124.95 1.18197 123.768 0 122.31 0C120.852 0 119.67 1.18197 119.67 2.64C119.67 4.09803 120.852 5.28 122.31 5.28Z" fill="#3B82F6"/>
|
||||||
|
<path d="M98.3802 5.29024C98.9048 5.29222 99.4181 5.13851 99.8552 4.84853C100.292 4.55855 100.634 4.14532 100.836 3.66127C101.038 3.17723 101.092 2.64412 100.991 2.12941C100.889 1.61471 100.638 1.14159 100.268 0.769978C99.8975 0.398368 99.4253 0.144953 98.911 0.0418894C98.3967 -0.0611745 97.8633 -0.00927669 97.3785 0.190998C96.8937 0.391273 96.4793 0.730946 96.1876 1.16695C95.896 1.60295 95.7402 2.11567 95.7402 2.64022C95.7402 3.34131 96.0181 4.01383 96.5129 4.51052C97.0078 5.0072 97.6792 5.2876 98.3802 5.29024V5.29024Z" fill="#3B82F6"/>
|
||||||
|
<path d="M74.4498 5.29023C74.9739 5.29023 75.4863 5.13483 75.9221 4.84364C76.3579 4.55245 76.6975 4.13859 76.8981 3.65437C77.0986 3.17015 77.1511 2.63729 77.0488 2.12324C76.9466 1.60919 76.6942 1.13698 76.3236 0.76637C75.953 0.395761 75.4808 0.143401 74.9668 0.04115C74.4527 -0.0611008 73.9199 -0.00860433 73.4357 0.191968C72.9514 0.39254 72.5376 0.732191 72.2465 1.16798C71.9553 1.60377 71.7998 2.11609 71.7998 2.64021C71.7998 2.98821 71.8683 3.33286 72.0015 3.65437C72.1346 3.97588 72.3299 4.26798 72.5759 4.51405C72.822 4.76013 73.1142 4.95534 73.4357 5.08851C73.7572 5.22169 74.1018 5.29023 74.4498 5.29023V5.29023Z" fill="#3B82F6"/>
|
||||||
|
<path d="M50.5101 5.28C51.9681 5.28 53.1501 4.09803 53.1501 2.64C53.1501 1.18197 51.9681 0 50.5101 0C49.0521 0 47.8701 1.18197 47.8701 2.64C47.8701 4.09803 49.0521 5.28 50.5101 5.28Z" fill="#3B82F6"/>
|
||||||
|
<path d="M27.592 4.97708C28.9406 4.42265 29.5843 2.87999 29.0298 1.53149C28.4754 0.182988 26.9328 -0.460681 25.5843 0.0937553C24.2358 0.648191 23.592 2.19079 24.1465 3.53929C24.7009 4.88779 26.2435 5.53152 27.592 4.97708Z" fill="#3B82F6"/>
|
||||||
|
<path d="M2.65002 5.29023C3.17414 5.29023 3.68652 5.13483 4.12231 4.84364C4.5581 4.55245 4.89769 4.13859 5.09827 3.65437C5.29884 3.17015 5.3514 2.63729 5.24915 2.12324C5.14689 1.60919 4.89453 1.13698 4.52393 0.76637C4.15332 0.395761 3.68104 0.143401 3.16699 0.04115C2.65294 -0.0611008 2.12021 -0.00860433 1.63599 0.191968C1.15176 0.39254 0.737841 0.732191 0.446655 1.16798C0.15547 1.60377 0 2.11609 0 2.64021C0 3.34303 0.279274 4.01708 0.776245 4.51405C1.27322 5.01102 1.9472 5.29023 2.65002 5.29023V5.29023Z" fill="#3B82F6"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_1384_22237">
|
||||||
|
<rect width="148.89" height="90.91" fill="white" transform="translate(0 108)"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="clip1_1384_22237">
|
||||||
|
<rect width="148.89" height="90.91" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 26 KiB |
44
basicswap/static/images/elements/dots-green.svg
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<svg width="149" height="91" viewBox="0 0 149 91" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_623_24537)">
|
||||||
|
<path d="M146.25 90.91C147.708 90.91 148.89 89.728 148.89 88.27C148.89 86.812 147.708 85.63 146.25 85.63C144.792 85.63 143.61 86.812 143.61 88.27C143.61 89.728 144.792 90.91 146.25 90.91Z" fill="#2AD168"/>
|
||||||
|
<path d="M122.31 90.91C123.768 90.91 124.95 89.728 124.95 88.27C124.95 86.812 123.768 85.63 122.31 85.63C120.852 85.63 119.67 86.812 119.67 88.27C119.67 89.728 120.852 90.91 122.31 90.91Z" fill="#2AD168"/>
|
||||||
|
<path d="M98.3798 90.91C98.9043 90.912 99.4176 90.7583 99.8547 90.4683C100.292 90.1783 100.633 89.7651 100.835 89.2811C101.037 88.797 101.091 88.2639 100.99 87.7492C100.889 87.2345 100.637 86.7614 100.267 86.3898C99.897 86.0181 99.4248 85.7647 98.9105 85.6617C98.3962 85.5586 97.8629 85.6105 97.3781 85.8108C96.8932 86.0111 96.4788 86.3507 96.1871 86.7867C95.8955 87.2227 95.7397 87.7355 95.7397 88.26C95.7397 88.9611 96.0176 89.6336 96.5125 90.1303C97.0073 90.627 97.6787 90.9074 98.3798 90.91V90.91Z" fill="#2AD168"/>
|
||||||
|
<path d="M74.4498 90.91C74.9739 90.91 75.4863 90.7546 75.9221 90.4634C76.3579 90.1722 76.6975 89.7584 76.8981 89.2742C77.0986 88.7899 77.1511 88.2571 77.0488 87.743C76.9466 87.229 76.6942 86.7568 76.3236 86.3862C75.953 86.0156 75.4808 85.7632 74.9668 85.6609C74.4527 85.5587 73.9199 85.6112 73.4357 85.8117C72.9514 86.0123 72.5376 86.352 72.2465 86.7878C71.9553 87.2236 71.7998 87.7359 71.7998 88.26C71.7998 88.608 71.8683 88.9526 72.0015 89.2742C72.1346 89.5957 72.3299 89.8878 72.5759 90.1338C72.822 90.3799 73.1142 90.5752 73.4357 90.7084C73.7572 90.8415 74.1018 90.91 74.4498 90.91V90.91Z" fill="#2AD168"/>
|
||||||
|
<path d="M50.5101 90.91C51.9681 90.91 53.1501 89.728 53.1501 88.27C53.1501 86.812 51.9681 85.63 50.5101 85.63C49.0521 85.63 47.8701 86.812 47.8701 88.27C47.8701 89.728 49.0521 90.91 50.5101 90.91Z" fill="#2AD168"/>
|
||||||
|
<path d="M27.5901 90.5955C28.9386 90.0411 29.5823 88.4984 29.0279 87.1499C28.4735 85.8014 26.9308 85.1578 25.5823 85.7122C24.2338 86.2666 23.5901 87.8092 24.1445 89.1577C24.6989 90.5062 26.2416 91.15 27.5901 90.5955Z" fill="#2AD168"/>
|
||||||
|
<path d="M2.65017 85.66C2.1252 85.658 1.61153 85.812 1.17421 86.1025C0.736895 86.3929 0.395678 86.8067 0.193868 87.2913C-0.00794293 87.7759 -0.0612624 88.3096 0.0406694 88.8246C0.142601 89.3396 0.395167 89.8126 0.766377 90.1838C1.13759 90.555 1.61065 90.8076 2.12563 90.9095C2.64061 91.0115 3.17433 90.9581 3.65896 90.7563C4.14359 90.5545 4.55737 90.2134 4.8478 89.7761C5.13822 89.3387 5.29216 88.825 5.29018 88.3C5.28755 87.6007 5.00858 86.9307 4.51406 86.4361C4.01953 85.9416 3.34953 85.6627 2.65017 85.66V85.66Z" fill="#2AD168"/>
|
||||||
|
<path d="M146.25 69.5C147.708 69.5 148.89 68.318 148.89 66.86C148.89 65.402 147.708 64.22 146.25 64.22C144.792 64.22 143.61 65.402 143.61 66.86C143.61 68.318 144.792 69.5 146.25 69.5Z" fill="#2AD168"/>
|
||||||
|
<path d="M122.31 69.5C123.768 69.5 124.95 68.318 124.95 66.86C124.95 65.402 123.768 64.22 122.31 64.22C120.852 64.22 119.67 65.402 119.67 66.86C119.67 68.318 120.852 69.5 122.31 69.5Z" fill="#2AD168"/>
|
||||||
|
<path d="M98.3798 69.5001C98.9047 69.502 99.4185 69.348 99.8559 69.0576C100.293 68.7672 100.634 68.3534 100.836 67.8688C101.038 67.3841 101.091 66.8505 100.989 66.3355C100.887 65.8205 100.635 65.3474 100.264 64.9762C99.8925 64.605 99.4193 64.3524 98.9043 64.2505C98.3893 64.1485 97.8557 64.2019 97.3711 64.4037C96.8865 64.6055 96.4727 64.9467 96.1823 65.384C95.8918 65.8213 95.7378 66.3351 95.7398 66.86C95.7424 67.5594 96.0215 68.2293 96.516 68.7239C97.0105 69.2184 97.6804 69.4974 98.3798 69.5001V69.5001Z" fill="#2AD168"/>
|
||||||
|
<path d="M74.4498 69.5001C74.9744 69.5 75.4871 69.3444 75.9231 69.0527C76.3591 68.7611 76.6988 68.3466 76.8991 67.8618C77.0993 67.377 77.1512 66.8437 77.0481 66.3293C76.9451 65.815 76.6917 65.3429 76.3201 64.9726C75.9485 64.6024 75.4753 64.3509 74.9606 64.2497C74.4459 64.1486 73.9128 64.2025 73.4287 64.4047C72.9447 64.6068 72.5316 64.948 72.2416 65.3851C71.9516 65.8222 71.7978 66.3355 71.7998 66.86C71.7998 67.2076 71.8684 67.5517 72.0017 67.8726C72.135 68.1936 72.3304 68.4851 72.5766 68.7303C72.8228 68.9756 73.115 69.1698 73.4364 69.3019C73.7579 69.434 74.1023 69.5014 74.4498 69.5001V69.5001Z" fill="#2AD168"/>
|
||||||
|
<path d="M50.5101 69.5C51.9681 69.5 53.1501 68.318 53.1501 66.86C53.1501 65.402 51.9681 64.22 50.5101 64.22C49.0521 64.22 47.8701 65.402 47.8701 66.86C47.8701 68.318 49.0521 69.5 50.5101 69.5Z" fill="#2AD168"/>
|
||||||
|
<path d="M27.5926 69.1932C28.9411 68.6387 29.5848 67.0961 29.0304 65.7476C28.4759 64.3991 26.9333 63.7554 25.5848 64.3098C24.2363 64.8642 23.5925 66.4069 24.147 67.7554C24.7014 69.1039 26.2441 69.7476 27.5926 69.1932Z" fill="#2AD168"/>
|
||||||
|
<path d="M2.65004 69.5001C3.17459 69.5 3.68731 69.3444 4.12331 69.0527C4.55931 68.7611 4.89899 68.3466 5.09926 67.8618C5.29954 67.377 5.3515 66.8437 5.24843 66.3293C5.14537 65.815 4.89189 65.3429 4.52028 64.9726C4.14867 64.6024 3.67561 64.3509 3.16091 64.2497C2.6462 64.1486 2.11309 64.2025 1.62905 64.4047C1.145 64.6068 0.731771 64.948 0.441791 65.3851C0.151812 65.8222 -0.00196055 66.3355 1.88721e-05 66.86C1.63788e-05 67.2076 0.0686259 67.5517 0.201923 67.8726C0.33522 68.1936 0.530667 68.4851 0.776874 68.7303C1.02308 68.9756 1.31528 69.1698 1.63674 69.3019C1.95819 69.434 2.30251 69.5014 2.65004 69.5001Z" fill="#2AD168"/>
|
||||||
|
<path d="M146.25 48.09C147.708 48.09 148.89 46.908 148.89 45.45C148.89 43.992 147.708 42.81 146.25 42.81C144.792 42.81 143.61 43.992 143.61 45.45C143.61 46.908 144.792 48.09 146.25 48.09Z" fill="#2AD168"/>
|
||||||
|
<path d="M122.31 48.09C123.768 48.09 124.95 46.908 124.95 45.45C124.95 43.992 123.768 42.81 122.31 42.81C120.852 42.81 119.67 43.992 119.67 45.45C119.67 46.908 120.852 48.09 122.31 48.09Z" fill="#2AD168"/>
|
||||||
|
<path d="M98.3798 48.1C98.9043 48.102 99.4176 47.9483 99.8547 47.6583C100.292 47.3683 100.633 46.9551 100.835 46.4711C101.037 45.987 101.091 45.4539 100.99 44.9392C100.889 44.4245 100.637 43.9514 100.267 43.5798C99.897 43.2082 99.4248 42.9547 98.9105 42.8517C98.3962 42.7486 97.8629 42.8005 97.3781 43.0008C96.8932 43.2011 96.4788 43.5407 96.1871 43.9767C95.8955 44.4127 95.7397 44.9255 95.7397 45.45C95.7397 46.1511 96.0176 46.8236 96.5125 47.3203C97.0073 47.817 97.6787 48.0974 98.3798 48.1V48.1Z" fill="#2AD168"/>
|
||||||
|
<path d="M74.4498 48.1C74.9739 48.1 75.4863 47.9446 75.9221 47.6534C76.3579 47.3622 76.6975 46.9484 76.8981 46.4642C77.0986 45.9799 77.1511 45.4471 77.0488 44.933C76.9466 44.419 76.6942 43.9468 76.3236 43.5762C75.953 43.2055 75.4808 42.9532 74.9668 42.8509C74.4527 42.7487 73.9199 42.8012 73.4357 43.0018C72.9514 43.2023 72.5376 43.542 72.2465 43.9778C71.9553 44.4136 71.7998 44.9259 71.7998 45.45C71.7998 45.798 71.8683 46.1426 72.0015 46.4642C72.1346 46.7857 72.3299 47.0778 72.5759 47.3238C72.822 47.5699 73.1142 47.7651 73.4357 47.8983C73.7572 48.0315 74.1018 48.1 74.4498 48.1V48.1Z" fill="#2AD168"/>
|
||||||
|
<path d="M50.5101 48.09C51.9681 48.09 53.1501 46.908 53.1501 45.45C53.1501 43.992 51.9681 42.81 50.5101 42.81C49.0521 42.81 47.8701 43.992 47.8701 45.45C47.8701 46.908 49.0521 48.09 50.5101 48.09Z" fill="#2AD168"/>
|
||||||
|
<path d="M27.5911 47.7915C28.9396 47.2371 29.5833 45.6944 29.0289 44.3459C28.4744 42.9974 26.9318 42.3537 25.5833 42.9082C24.2348 43.4626 23.591 45.0052 24.1455 46.3537C24.6999 47.7022 26.2426 48.3459 27.5911 47.7915Z" fill="#2AD168"/>
|
||||||
|
<path d="M2.65002 48.1C3.17414 48.1 3.68652 47.9446 4.12231 47.6534C4.5581 47.3622 4.89769 46.9484 5.09827 46.4642C5.29884 45.9799 5.3514 45.4471 5.24915 44.933C5.14689 44.419 4.89453 43.9468 4.52393 43.5762C4.15332 43.2055 3.68104 42.9532 3.16699 42.8509C2.65294 42.7487 2.12021 42.8012 1.63599 43.0018C1.15176 43.2023 0.737841 43.542 0.446655 43.9778C0.15547 44.4136 0 44.9259 0 45.45C0 46.1528 0.279274 46.8269 0.776245 47.3238C1.27322 47.8208 1.9472 48.1 2.65002 48.1V48.1Z" fill="#2AD168"/>
|
||||||
|
<path d="M146.25 26.69C147.708 26.69 148.89 25.508 148.89 24.05C148.89 22.592 147.708 21.41 146.25 21.41C144.792 21.41 143.61 22.592 143.61 24.05C143.61 25.508 144.792 26.69 146.25 26.69Z" fill="#2AD168"/>
|
||||||
|
<path d="M122.31 26.69C123.768 26.69 124.95 25.508 124.95 24.05C124.95 22.592 123.768 21.41 122.31 21.41C120.852 21.41 119.67 22.592 119.67 24.05C119.67 25.508 120.852 26.69 122.31 26.69Z" fill="#2AD168"/>
|
||||||
|
<path d="M98.3798 26.66C98.9047 26.662 99.4185 26.508 99.8559 26.2176C100.293 25.9272 100.634 25.5134 100.836 25.0287C101.038 24.5441 101.091 24.0105 100.989 23.4955C100.887 22.9805 100.635 22.5074 100.264 22.1362C99.8925 21.765 99.4193 21.5124 98.9043 21.4105C98.3893 21.3085 97.8557 21.3618 97.3711 21.5636C96.8865 21.7655 96.4727 22.1067 96.1823 22.544C95.8918 22.9813 95.7378 23.495 95.7398 24.02C95.7424 24.7194 96.0215 25.3894 96.516 25.8839C97.0105 26.3784 97.6804 26.6574 98.3798 26.66V26.66Z" fill="#2AD168"/>
|
||||||
|
<path d="M74.4498 26.66C74.9744 26.66 75.4871 26.5043 75.9231 26.2127C76.3591 25.9211 76.6988 25.5066 76.8991 25.0218C77.0993 24.537 77.1512 24.0037 77.0481 23.4894C76.9451 22.975 76.6917 22.5029 76.3201 22.1327C75.9485 21.7625 75.4753 21.5108 74.9606 21.4097C74.4459 21.3086 73.9128 21.3625 73.4287 21.5646C72.9447 21.7667 72.5316 22.1079 72.2416 22.545C71.9516 22.9821 71.7978 23.4955 71.7998 24.02C71.7998 24.3675 71.8684 24.7117 72.0017 25.0326C72.135 25.3536 72.3304 25.645 72.5766 25.8903C72.8228 26.1356 73.115 26.3299 73.4364 26.462C73.7579 26.594 74.1023 26.6613 74.4498 26.66V26.66Z" fill="#2AD168"/>
|
||||||
|
<path d="M50.5101 26.69C51.9681 26.69 53.1501 25.508 53.1501 24.05C53.1501 22.592 51.9681 21.41 50.5101 21.41C49.0521 21.41 47.8701 22.592 47.8701 24.05C47.8701 25.508 49.0521 26.69 50.5101 26.69Z" fill="#2AD168"/>
|
||||||
|
<path d="M27.5935 26.3891C28.942 25.8347 29.5858 24.292 29.0313 22.9435C28.4769 21.595 26.9342 20.9513 25.5857 21.5057C24.2372 22.0602 23.5935 23.6028 24.1479 24.9513C24.7024 26.2998 26.245 26.9436 27.5935 26.3891Z" fill="#2AD168"/>
|
||||||
|
<path d="M2.65004 26.66C3.17459 26.66 3.68731 26.5043 4.12331 26.2127C4.55931 25.9211 4.89899 25.5066 5.09926 25.0218C5.29954 24.537 5.3515 24.0037 5.24843 23.4894C5.14537 22.975 4.89189 22.5029 4.52028 22.1327C4.14867 21.7625 3.67561 21.5108 3.16091 21.4097C2.6462 21.3086 2.11309 21.3625 1.62905 21.5646C1.145 21.7667 0.731771 22.1079 0.441791 22.545C0.151812 22.9821 -0.00196055 23.4955 1.88721e-05 24.02C1.63788e-05 24.3675 0.0686259 24.7117 0.201923 25.0326C0.33522 25.3536 0.530667 25.645 0.776874 25.8903C1.02308 26.1356 1.31528 26.3299 1.63674 26.462C1.95819 26.594 2.30251 26.6613 2.65004 26.66V26.66Z" fill="#2AD168"/>
|
||||||
|
<path d="M146.25 5.28C147.708 5.28 148.89 4.09803 148.89 2.64C148.89 1.18197 147.708 0 146.25 0C144.792 0 143.61 1.18197 143.61 2.64C143.61 4.09803 144.792 5.28 146.25 5.28Z" fill="#2AD168"/>
|
||||||
|
<path d="M122.31 5.28C123.768 5.28 124.95 4.09803 124.95 2.64C124.95 1.18197 123.768 0 122.31 0C120.852 0 119.67 1.18197 119.67 2.64C119.67 4.09803 120.852 5.28 122.31 5.28Z" fill="#2AD168"/>
|
||||||
|
<path d="M98.3798 5.29003C98.9043 5.29201 99.4176 5.1383 99.8547 4.84832C100.292 4.55834 100.633 4.14511 100.835 3.66106C101.037 3.17701 101.091 2.64391 100.99 2.1292C100.889 1.61449 100.637 1.14137 100.267 0.769764C99.897 0.398154 99.4248 0.14474 98.9105 0.0416758C98.3962 -0.0613881 97.8629 -0.00949031 97.3781 0.190785C96.8932 0.39106 96.4788 0.730733 96.1871 1.16674C95.8955 1.60274 95.7397 2.11546 95.7397 2.64C95.7397 3.3411 96.0176 4.01362 96.5125 4.5103C97.0073 5.00699 97.6787 5.28738 98.3798 5.29003V5.29003Z" fill="#2AD168"/>
|
||||||
|
<path d="M74.4498 5.29002C74.9739 5.29002 75.4863 5.13461 75.9221 4.84343C76.3579 4.55224 76.6975 4.13838 76.8981 3.65416C77.0986 3.16993 77.1511 2.63708 77.0488 2.12303C76.9466 1.60898 76.6942 1.13677 76.3236 0.766156C75.953 0.395547 75.4808 0.143187 74.9668 0.0409364C74.4527 -0.0613144 73.9199 -0.00881796 73.4357 0.191754C72.9514 0.392326 72.5376 0.731977 72.2465 1.16777C71.9553 1.60356 71.7998 2.11588 71.7998 2.64C71.7998 2.988 71.8683 3.33264 72.0015 3.65416C72.1346 3.97567 72.3299 4.26776 72.5759 4.51384C72.822 4.75991 73.1142 4.95512 73.4357 5.0883C73.7572 5.22147 74.1018 5.29002 74.4498 5.29002V5.29002Z" fill="#2AD168"/>
|
||||||
|
<path d="M50.5101 5.28C51.9681 5.28 53.1501 4.09803 53.1501 2.64C53.1501 1.18197 51.9681 0 50.5101 0C49.0521 0 47.8701 1.18197 47.8701 2.64C47.8701 4.09803 49.0521 5.28 50.5101 5.28Z" fill="#2AD168"/>
|
||||||
|
<path d="M27.592 4.97745C28.9406 4.42301 29.5843 2.88036 29.0298 1.53186C28.4754 0.183354 26.9328 -0.460315 25.5843 0.0941215C24.2358 0.648558 23.592 2.19116 24.1465 3.53966C24.7009 4.88816 26.2435 5.53189 27.592 4.97745Z" fill="#2AD168"/>
|
||||||
|
<path d="M2.65002 5.29002C3.17414 5.29002 3.68652 5.13461 4.12231 4.84343C4.5581 4.55224 4.89769 4.13838 5.09827 3.65416C5.29884 3.16993 5.3514 2.63708 5.24915 2.12303C5.14689 1.60898 4.89453 1.13677 4.52393 0.766156C4.15332 0.395547 3.68104 0.143187 3.16699 0.0409364C2.65294 -0.0613144 2.12021 -0.00881796 1.63599 0.191754C1.15176 0.392326 0.737841 0.731977 0.446655 1.16777C0.15547 1.60356 0 2.11588 0 2.64C0 3.34282 0.279274 4.01687 0.776245 4.51384C1.27322 5.01081 1.9472 5.29002 2.65002 5.29002V5.29002Z" fill="#2AD168"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_623_24537">
|
||||||
|
<rect width="148.89" height="90.91" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 13 KiB |