Compare commits
297 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fb4f00fd9 | ||
|
|
e2fe0697ee | ||
|
|
0a5680da13 | ||
|
|
264f4d209f | ||
|
|
3ee69ea11a | ||
|
|
c523754516 | ||
|
|
13015e3da9 | ||
|
|
f7141dd0c9 | ||
|
|
3430776ffc | ||
|
|
48a46aea47 | ||
|
|
eb7f3b54ec | ||
|
|
c43d46c7e8 | ||
|
|
7d77d46fa2 | ||
|
|
934e809ac3 | ||
|
|
8081f22e92 | ||
|
|
c9b99dd67a | ||
|
|
fe83736ec7 | ||
|
|
817d2c1e9c | ||
|
|
c0d9b7c161 | ||
|
|
014ee22b79 | ||
|
|
cbd0898eb1 | ||
|
|
63d27b4a6f | ||
|
|
fdfa03eaaf | ||
|
|
c9d1129e93 | ||
|
|
376b485261 | ||
|
|
75fa008f0a | ||
|
|
54983913e1 | ||
|
|
c6f3c684a8 | ||
|
|
39aad231cd | ||
|
|
bd06c435e9 | ||
|
|
fb1caea4de | ||
|
|
d8430f4ca9 | ||
|
|
f7315d405d | ||
|
|
bcd251c4df | ||
|
|
3f963f3329 | ||
|
|
4117c461bb | ||
|
|
8c6ea947ba | ||
|
|
f2a3fc1da1 | ||
|
|
771ad2586a | ||
|
|
60a3956c07 | ||
|
|
d097846756 | ||
|
|
1209d1b269 | ||
|
|
209dea52b3 | ||
|
|
f954822d74 | ||
|
|
dbdb89cd10 | ||
|
|
c53e426989 | ||
|
|
484ad0ca38 | ||
|
|
ac7f24daff | ||
|
|
c1f724ac5e | ||
|
|
d5f643aab9 | ||
|
|
72fc065928 | ||
|
|
25b479fdb6 | ||
|
|
4b18ddfe79 | ||
|
|
bdea7de27e | ||
|
|
bcd9d5c9af | ||
|
|
fe976810e3 | ||
|
|
033167a451 | ||
|
|
cdfb9132ad | ||
|
|
b6d29a33d2 | ||
|
|
003d7b85ab | ||
|
|
1b585ea5c9 | ||
|
|
47a80dc603 | ||
|
|
b29d37a8be | ||
|
|
60369fc2a4 | ||
|
|
3927d823c0 | ||
|
|
1d58dfdc94 | ||
|
|
7ac75f7344 | ||
|
|
80c43056cc | ||
|
|
1564655777 | ||
|
|
2d243fc310 | ||
|
|
75ad5a5b4d | ||
|
|
5e69bf172c | ||
|
|
f307332409 | ||
|
|
56378d168b | ||
|
|
274be9d716 | ||
|
|
eec0760e44 | ||
|
|
6ed108e741 | ||
|
|
7429dc5b2d | ||
|
|
9a900a5bac | ||
|
|
ba4796c763 | ||
|
|
57f238d48e | ||
|
|
d93a73c29e | ||
|
|
94303cff93 | ||
|
|
a977cfe857 | ||
|
|
00912b277a | ||
|
|
40eff0ce0f | ||
|
|
9835d33d12 | ||
|
|
64165e387e | ||
|
|
34d94760a3 | ||
|
|
713990f57b | ||
|
|
58544d141d | ||
|
|
e125aa33d2 | ||
|
|
fd7977b35a | ||
|
|
dc4f0ac2d3 | ||
|
|
ee2f462ee9 | ||
|
|
c3cd1871ef | ||
|
|
3e4c3f10cf | ||
|
|
80852fd0ea | ||
|
|
ad7d23a8de | ||
|
|
166b035983 | ||
|
|
c27ac833d1 | ||
|
|
e62e9eb0bf | ||
|
|
b07bc3c456 | ||
|
|
ebdbe115dd | ||
|
|
5f6819afcb | ||
|
|
ae1df0b556 | ||
|
|
d3e3c3c95b | ||
|
|
adc80eabb0 | ||
|
|
42fa4d49d4 | ||
|
|
b077561a6f | ||
|
|
57bc1d5ccf | ||
|
|
62aa1fa5d7 | ||
|
|
73b4b2a46b | ||
|
|
76445146fb | ||
|
|
fcf234ef34 | ||
|
|
aa1e1fd79c | ||
|
|
446d6fe357 | ||
|
|
2a8c04b285 | ||
|
|
76879a2ff5 | ||
|
|
d527ec4974 | ||
|
|
74c7072926 | ||
|
|
ab472c04be | ||
|
|
150caeec40 | ||
|
|
761d0ca505 | ||
|
|
9160bfe452 | ||
|
|
942b436974 | ||
|
|
6ac9bbb19c | ||
|
|
047fe7ba27 | ||
|
|
74ce19052d | ||
|
|
5e8547063e | ||
|
|
80a8f8967f | ||
|
|
902d9ff13b | ||
|
|
96363136d2 | ||
|
|
e63014026d | ||
|
|
46d0bdde4b | ||
|
|
47f7b4545e | ||
|
|
f64b2c1030 | ||
|
|
8d8743074e | ||
|
|
a08bdfbdb8 | ||
|
|
6df09a973e | ||
|
|
450b562db9 | ||
|
|
af26b5d2fe | ||
|
|
48f406a338 | ||
|
|
e87d54a259 | ||
|
|
ab80b9b7f5 | ||
|
|
842c4f54ac | ||
|
|
a01202dcb2 | ||
|
|
e6c79a6743 | ||
|
|
926f6e1a76 | ||
|
|
343fd6efbc | ||
|
|
e20516ef71 | ||
|
|
8a279dc71f | ||
|
|
463c51c822 | ||
|
|
360d29128c | ||
|
|
2cf279f27f | ||
|
|
1cbc2f44b0 | ||
|
|
2a28f336e2 | ||
|
|
10a416aa04 | ||
|
|
e6d4ab500b | ||
|
|
adcf875db6 | ||
|
|
a85fbce4ae | ||
|
|
109a4383ea | ||
|
|
1dc3c1c7ae | ||
|
|
e90292fb2f | ||
|
|
d83555c25f | ||
|
|
00c8af6853 | ||
|
|
3b73894ce8 | ||
|
|
e49ffbfdf7 | ||
|
|
594845e312 | ||
|
|
7ccc84f265 | ||
|
|
f903038964 | ||
|
|
31a1224311 | ||
|
|
4711d72bf5 | ||
|
|
44f16bd28e | ||
|
|
a486930cd5 | ||
|
|
00b8443aff | ||
|
|
6739e2bc5f | ||
|
|
d71fa04781 | ||
|
|
f4c88b5356 | ||
|
|
d6535dbc1d | ||
|
|
18d8dfd3fc | ||
|
|
8f1c3e648c | ||
|
|
650aa9d72f | ||
|
|
c4ae489c45 | ||
|
|
b38a7828ff | ||
|
|
2c8e8f3e30 | ||
|
|
18d4a9105d | ||
|
|
e464599cf7 | ||
|
|
69b195e317 | ||
|
|
13ae72b38f | ||
|
|
acc135d22e | ||
|
|
c5cf6fb132 | ||
|
|
49e4072f2a | ||
|
|
4f114ba9ae | ||
|
|
37651598bf | ||
|
|
a78880bc98 | ||
|
|
b0d169421f | ||
|
|
a1bcf8d4b9 | ||
|
|
42421321f6 | ||
|
|
483d77a0c6 | ||
|
|
0d344a907c | ||
|
|
091e8da5e2 | ||
|
|
f091d17560 | ||
|
|
73d4ade5f6 | ||
|
|
2793ee00dd | ||
|
|
b96a823536 | ||
|
|
7d850406ca | ||
|
|
4e7a6e994d | ||
|
|
b55042bf07 | ||
|
|
9be4bd28fd | ||
|
|
e9986148d7 | ||
|
|
0aaf3f8bcc | ||
|
|
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 |
13
.cirrus.yml
@@ -21,14 +21,12 @@ test_task:
|
||||
- XMR_BINDIR: ${BIN_DIR}/monero
|
||||
setup_script:
|
||||
- apt-get update
|
||||
- apt-get install -y wget python3-pip gnupg unzip protobuf-compiler automake libtool pkg-config
|
||||
- pip install tox pytest
|
||||
- apt-get install -y git python3-pip gnupg automake libtool pkg-config
|
||||
- pip install tox pytest wheel
|
||||
- python3 setup.py install
|
||||
- wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_v0.1.zip
|
||||
- unzip -d coincurve-anonswap coincurve-anonswap.zip
|
||||
- mv ./coincurve-anonswap/*/{.,}* ./coincurve-anonswap || true
|
||||
- cd coincurve-anonswap
|
||||
- python3 setup.py install --force
|
||||
- git clone https://github.com/basicswap/coincurve.git -b basicswap_v0.2 coincurve-basicswap
|
||||
- cd coincurve-basicswap
|
||||
- pip install .
|
||||
bins_cache:
|
||||
folder: /tmp/cached_bin
|
||||
reupload_on_changes: false
|
||||
@@ -47,3 +45,4 @@ test_task:
|
||||
- pytest tests/basicswap/test_other.py
|
||||
- pytest tests/basicswap/test_run.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'
|
||||
|
||||
26
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: lint
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.12"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install flake8 codespell
|
||||
- name: Running flake8
|
||||
run: |
|
||||
PYTHONWARNINGS="ignore" flake8 --ignore=E501,F841,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,messages_pb2.py,.eggs,.tox,bin/install_certifi.py
|
||||
- name: Running codespell
|
||||
run: |
|
||||
codespell --check-filenames --disable-colors --quiet-level=7 --ignore-words=tests/lint/spelling.ignore-words.txt -S .git,.eggs,.tox,pgp,*.pyc,*basicswap/contrib,*basicswap/interface/contrib,*mnemonics.py,bin/install_certifi.py,*basicswap/static
|
||||
4
.gitignore
vendored
@@ -9,3 +9,7 @@ __pycache__
|
||||
.tox
|
||||
.eggs
|
||||
*~
|
||||
|
||||
# geckodriver.log
|
||||
*.log
|
||||
docker/.env
|
||||
|
||||
60
.travis.yml
@@ -1,60 +0,0 @@
|
||||
dist: bionic
|
||||
os: linux
|
||||
language: python
|
||||
python: '3.7'
|
||||
stages:
|
||||
- lint
|
||||
- test
|
||||
env:
|
||||
global:
|
||||
- TEST_DIR=${HOME}/test_basicswap2
|
||||
- TEST_RELOAD_PATH=~/test_basicswap1
|
||||
- BIN_DIR=~/cached_bin
|
||||
- PARTICL_BINDIR=${BIN_DIR}/particl
|
||||
- BITCOIN_BINDIR=${BIN_DIR}/bitcoin
|
||||
- LITECOIN_BINDIR=${BIN_DIR}/litecoin
|
||||
- XMR_BINDIR=${BIN_DIR}/monero
|
||||
cache:
|
||||
directories:
|
||||
- "$BIN_DIR"
|
||||
before_install:
|
||||
- sudo apt-get install -y wget python3-pip gnupg unzip protobuf-compiler automake libtool pkg-config
|
||||
install:
|
||||
- travis_retry pip install tox pytest
|
||||
before_script:
|
||||
- wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_v0.1.zip
|
||||
- unzip -d coincurve-anonswap coincurve-anonswap.zip
|
||||
- mv ./coincurve-anonswap/*/{.,}* ./coincurve-anonswap || true
|
||||
- cd coincurve-anonswap
|
||||
- python3 setup.py install --force
|
||||
script:
|
||||
- cd $TRAVIS_BUILD_DIR
|
||||
- python3 setup.py install
|
||||
- basicswap-prepare --bindir=${BIN_DIR} --preparebinonly --withcoins=particl,bitcoin,litecoin,monero
|
||||
- export DATADIRS="${TEST_DIR}"
|
||||
- mkdir -p "${DATADIRS}/bin"
|
||||
- cp -r ${BIN_DIR} "${DATADIRS}/bin"
|
||||
- mkdir -p "${TEST_RELOAD_PATH}/bin"
|
||||
- cp -r ${BIN_DIR} "${TEST_RELOAD_PATH}/bin"
|
||||
- # tox
|
||||
- pytest tests/basicswap/test_xmr.py
|
||||
- pytest tests/basicswap/test_xmr_reload.py
|
||||
- pytest tests/basicswap/test_xmr_bids_offline.py
|
||||
after_success:
|
||||
- echo "End test"
|
||||
jobs:
|
||||
include:
|
||||
- stage: lint
|
||||
env:
|
||||
cache: false
|
||||
install:
|
||||
- travis_retry pip install flake8==3.7.0
|
||||
- travis_retry pip install codespell==1.15.0
|
||||
before_script:
|
||||
script:
|
||||
- PYTHONWARNINGS="ignore" flake8 --ignore=E501,F841,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,messages_pb2.py,.eggs,.tox,bin/install_certifi.py
|
||||
- codespell --check-filenames --disable-colors --quiet-level=7 --ignore-words=tests/lint/spelling.ignore-words.txt -S .git,.eggs,.tox,pgp,*.pyc,*basicswap/contrib,*basicswap/interface/contrib,*mnemonics.py,bin/install_certifi.py,*basicswap/static
|
||||
after_success:
|
||||
- echo "End lint"
|
||||
- stage: test
|
||||
env:
|
||||
21
Dockerfile
@@ -5,22 +5,12 @@ ENV LANG=C.UTF-8 \
|
||||
DATADIRS="/coindata"
|
||||
|
||||
RUN apt-get update; \
|
||||
apt-get install -y wget python3-pip gnupg unzip make g++ autoconf automake libtool pkg-config gosu tzdata;
|
||||
apt-get install -y git python3-pip gnupg make g++ autoconf automake libtool pkg-config gosu tzdata;
|
||||
|
||||
# Must install protoc directly as latest package is only on 3.12
|
||||
RUN wget -O protobuf_src.tar.gz https://github.com/protocolbuffers/protobuf/releases/download/v21.1/protobuf-python-4.21.1.tar.gz && \
|
||||
tar xvf protobuf_src.tar.gz && \
|
||||
cd protobuf-3.21.1 && \
|
||||
./configure --prefix=/usr && \
|
||||
make -j$(nproc) install && \
|
||||
ldconfig
|
||||
|
||||
ARG COINCURVE_VERSION=v0.1
|
||||
RUN wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_$COINCURVE_VERSION.zip && \
|
||||
unzip coincurve-anonswap.zip && \
|
||||
mv ./coincurve-anonswap_$COINCURVE_VERSION ./coincurve-anonswap && \
|
||||
cd coincurve-anonswap && \
|
||||
python3 setup.py install --force
|
||||
ARG COINCURVE_VERSION=v0.2
|
||||
RUN git clone https://github.com/basicswap/coincurve.git -b basicswap_$COINCURVE_VERSION coincurve-basicswap && \
|
||||
cd coincurve-basicswap && \
|
||||
pip install .
|
||||
|
||||
# Install requirements first so as to skip in subsequent rebuilds
|
||||
COPY ./requirements.txt requirements.txt
|
||||
@@ -28,7 +18,6 @@ RUN pip3 install -r requirements.txt
|
||||
|
||||
COPY . basicswap-master
|
||||
RUN cd basicswap-master; \
|
||||
protoc -I=basicswap --python_out=basicswap basicswap/messages.proto; \
|
||||
pip3 install .;
|
||||
|
||||
RUN useradd -ms /bin/bash swap_user && \
|
||||
|
||||
2
LICENSE
@@ -1,5 +1,5 @@
|
||||
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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
include *.md LICENSE
|
||||
|
||||
recursive-include doc *
|
||||
recursive-include pgp *
|
||||
recursive-include basicswap/templates *
|
||||
recursive-include basicswap/static *
|
||||
recursive-include basicswap/contrib/mnemonic/wordlist *
|
||||
|
||||
58
README.md
@@ -15,27 +15,31 @@ Table of Contents
|
||||
|
||||
## About
|
||||
|
||||
The BasicSwap DEX is a privacy-first and decentralized exchange which features cross-chain atomic swaps and a distributed order book.
|
||||
**BasicSwap** is the world’s most secure and decentralized DEX. It facilitates cross-chain atomic swaps by enabling peers to interact directly with each other within a free and open environment without central points of failure.
|
||||
|
||||
[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.
|
||||
This DEX is fully non-custodial and features a decentralized order book, letting you create or accept swap offers without any fees, counterparties, or the need for accounts.
|
||||
|
||||
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.
|
||||
Built as a low-friction, highly secure solution to the frequent losses of funds on centralized exchanges (e.g., FTX, BitFinex, MtGox), **BasicSwap** aims to provide more reliable and secure cryptocurrency trading conditions for everyone.
|
||||
|
||||
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!
|
||||
**BasicSwap** is currently in active development by the community. While it already offers some of the essential trading features you'd expect from an exchange, more features and quality-of-life improvements are being worked on with the goal to provide a smoother user experience.
|
||||
|
||||
## 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.
|
||||
* **Decentralized order book** — Make or take swap offers on a completely decentralized order book system.
|
||||
* **No third-party or middleman** — Trade crypto with no intermediaries, completely eliminating central points of failure.
|
||||
* **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.
|
||||
* **Superior financial privacy** — Protect your financial information from unauthorized access with BasicSwap’s privacy-conscious technology.
|
||||
* **Full Monero support** — Swap Monero with a variety of other cryptocurrencies like Bitcoin or Particl. No wrapped assets or layer-2 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.
|
||||
## Under the Hood
|
||||
|
||||
**BasicSwap** can be best understood as the decentralized version of the SWIFT messaging network; providing a decentralized messaging protocol that allows for peers to connect directly with each other with the purpose of executing atomic swaps without central points of failure and using official core wallets (Bitcoin Core, Litecoin Core, etc).
|
||||
|
||||
**BasicSwap** does not process, initiate, or execute swaps; it merely enables peers to communicate with each other and exchange the required information to simplify the process of using atomic swaps on the respective blockchains of the coins being swapped.
|
||||
|
||||
In essence, **BasicSwap** operates merely as a decentralized messaging protocol supplemented by a user-friendly interface.
|
||||
|
||||
## Available Assets
|
||||
|
||||
@@ -84,6 +88,18 @@ BasicSwap is compatible with the following digital assets.
|
||||
<td>PIVX
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Decred
|
||||
</td>
|
||||
<td>DCR
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Wownero
|
||||
</td>
|
||||
<td>WOW
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Particl
|
||||
</td>
|
||||
@@ -92,39 +108,31 @@ BasicSwap is compatible with the following digital assets.
|
||||
</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.
|
||||
If you’d like to add a cryptocurrency to BasicSwap, refer to how other cryptocurrencies have been integrated to the DEX by following [this link](https://academy.particl.io/en/latest/basicswap-guides/basicswapguides_apply.html).
|
||||
|
||||
# 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).
|
||||
* **For support** Join the community on [#basicswap:matrix.org](https://matrix.to/#/#basicswap:matrix.org) using a Matrix client.
|
||||
|
||||
[](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.
|
||||
Follow the guides on [Particl Academy](https://academy.particl.io) for tutorials and guides on how BasicSwap works.
|
||||
|
||||
* [Download BasicSwapDEX](https://github.com/tecnovert/basicswap/tree/master/doc)
|
||||
* [Download BasicSwapDEX](https://github.com/basicswap/basicswap/tree/master/doc)
|
||||
|
||||
#### Community chat support
|
||||
|
||||
* [Discord](https://discord.me/particl) navigate to the #support channel
|
||||
|
||||
* [Telegram](https://t.me/particlhelp)
|
||||
|
||||
* [Element](https://app.element.io/#/room/#particlhelp:matrix.org)
|
||||
* [Matrix](https://matrix.to/#/#basicswap: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!
|
||||
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 channel; you’ll be sure to find help and support from current contributors there!
|
||||
|
||||
# License
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
name = "basicswap"
|
||||
|
||||
__version__ = "0.11.68"
|
||||
__version__ = "0.13.5"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019-2023 tecnovert
|
||||
# Copyright (c) 2019-2024 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -8,6 +8,7 @@ import os
|
||||
import time
|
||||
import shlex
|
||||
import socks
|
||||
import random
|
||||
import socket
|
||||
import urllib
|
||||
import logging
|
||||
@@ -45,7 +46,7 @@ class BaseApp:
|
||||
self.settings = settings
|
||||
self.coin_clients = {}
|
||||
self.coin_interfaces = {}
|
||||
self.mxDB = threading.RLock()
|
||||
self.mxDB = threading.Lock()
|
||||
self.debug = self.settings.get('debug', False)
|
||||
self.delay_event = threading.Event()
|
||||
self.chainstate_delay_event = threading.Event()
|
||||
@@ -76,10 +77,10 @@ class BaseApp:
|
||||
# Remove any existing 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()
|
||||
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:
|
||||
stream_stdout.setFormatter(formatter)
|
||||
stream_fp = logging.StreamHandler(self.fp)
|
||||
@@ -161,12 +162,13 @@ class BaseApp:
|
||||
socket.getaddrinfo = self.default_socket_getaddrinfo
|
||||
socket.setdefaulttimeout(self.default_socket_timeout)
|
||||
|
||||
def readURL(self, url: str, timeout: int = 120, headers=None) -> bytes:
|
||||
def readURL(self, url: str, timeout: int = 120, headers={}) -> bytes:
|
||||
open_handler = None
|
||||
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')]
|
||||
if headers is None:
|
||||
opener.addheaders = [('User-agent', 'Mozilla/5.0')]
|
||||
request = urllib.request.Request(url, headers=headers)
|
||||
return opener.open(request, timeout=timeout).read()
|
||||
|
||||
@@ -198,3 +200,28 @@ class BaseApp:
|
||||
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 -*-
|
||||
|
||||
# Copyright (c) 2021-2023 tecnovert
|
||||
# Copyright (c) 2021-2024 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -70,6 +70,7 @@ class OfferStates(IntEnum):
|
||||
OFFER_SENT = 1
|
||||
OFFER_RECEIVED = 2
|
||||
OFFER_ABANDONED = 3
|
||||
OFFER_EXPIRED = 4
|
||||
|
||||
|
||||
class BidStates(IntEnum):
|
||||
@@ -103,6 +104,7 @@ class BidStates(IntEnum):
|
||||
XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX = 28 # XmrBidLockSpendTxMessage
|
||||
BID_REQUEST_SENT = 29
|
||||
BID_REQUEST_ACCEPTED = 30
|
||||
BID_EXPIRED = 31
|
||||
|
||||
|
||||
class TxStates(IntEnum):
|
||||
@@ -180,6 +182,7 @@ class EventLogTypes(IntEnum):
|
||||
PTX_PUBLISHED = auto()
|
||||
PTX_REDEEM_PUBLISHED = auto()
|
||||
PTX_REFUND_PUBLISHED = auto()
|
||||
LOCK_TX_B_IN_MEMPOOL = auto()
|
||||
|
||||
|
||||
class XmrSplitMsgTypes(IntEnum):
|
||||
@@ -198,6 +201,9 @@ class DebugTypes(IntEnum):
|
||||
SKIP_LOCK_TX_REFUND = auto()
|
||||
SEND_LOCKED_XMR = auto()
|
||||
B_LOCK_TX_MISSED_SEND = auto()
|
||||
DUPLICATE_ACTIONS = auto()
|
||||
DONT_CONFIRM_PTX = auto()
|
||||
OFFER_LOCK_2_VALUE_INC = auto()
|
||||
|
||||
|
||||
class NotificationTypes(IntEnum):
|
||||
@@ -246,6 +252,8 @@ def strOfferState(state):
|
||||
return 'Received'
|
||||
if state == OfferStates.OFFER_ABANDONED:
|
||||
return 'Abandoned'
|
||||
if state == OfferStates.OFFER_EXPIRED:
|
||||
return 'Expired'
|
||||
return 'Unknown'
|
||||
|
||||
|
||||
@@ -310,7 +318,8 @@ def strBidState(state):
|
||||
return 'Request accepted'
|
||||
if state == BidStates.BID_STATE_UNKNOWN:
|
||||
return 'Unknown bid state'
|
||||
|
||||
if state == BidStates.BID_EXPIRED:
|
||||
return 'Expired'
|
||||
return 'Unknown' + ' ' + str(state)
|
||||
|
||||
|
||||
@@ -390,6 +399,8 @@ def describeEventEntry(event_type, event_msg):
|
||||
return 'Lock tx B seen in chain'
|
||||
if event_type == EventLogTypes.LOCK_TX_B_CONFIRMED:
|
||||
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:
|
||||
return 'Debug tweak applied ' + event_msg
|
||||
if event_type == EventLogTypes.FAILED_TX_B_REFUND:
|
||||
@@ -464,7 +475,7 @@ def getOfferProofOfFundsHash(offer_msg, offer_addr):
|
||||
# TODO: Hash must not include proof_of_funds sig if it exists in offer_msg
|
||||
h = hashlib.sha256()
|
||||
h.update(offer_addr.encode('utf-8'))
|
||||
offer_bytes = offer_msg.SerializeToString()
|
||||
offer_bytes = offer_msg.to_bytes()
|
||||
h.update(offer_bytes)
|
||||
return h.digest()
|
||||
|
||||
@@ -499,7 +510,7 @@ def strSwapDesc(swap_type):
|
||||
return None
|
||||
|
||||
|
||||
inactive_states = [BidStates.SWAP_COMPLETED, BidStates.BID_ERROR, BidStates.BID_REJECTED, BidStates.SWAP_TIMEDOUT, BidStates.BID_ABANDONED]
|
||||
inactive_states = [BidStates.SWAP_COMPLETED, BidStates.BID_ERROR, BidStates.BID_REJECTED, BidStates.SWAP_TIMEDOUT, BidStates.BID_ABANDONED, BidStates.BID_EXPIRED]
|
||||
|
||||
|
||||
def isActiveBidState(state):
|
||||
|
||||
@@ -1,37 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019-2023 tecnovert
|
||||
# Copyright (c) 2019-2024 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import threading
|
||||
|
||||
from enum import IntEnum
|
||||
from .util import (
|
||||
COIN,
|
||||
make_int,
|
||||
format_amount,
|
||||
TemporaryError,
|
||||
)
|
||||
|
||||
XMR_COIN = 10 ** 12
|
||||
WOW_COIN = 10 ** 11
|
||||
|
||||
|
||||
class Coins(IntEnum):
|
||||
PART = 1
|
||||
BTC = 2
|
||||
LTC = 3
|
||||
# DCR = 4
|
||||
DCR = 4
|
||||
NMC = 5
|
||||
XMR = 6
|
||||
PART_BLIND = 7
|
||||
PART_ANON = 8
|
||||
# ZANO = 9
|
||||
WOW = 9
|
||||
# NDAU = 10
|
||||
PIVX = 11
|
||||
DASH = 12
|
||||
FIRO = 13
|
||||
NAV = 14
|
||||
LTC_MWEB = 15
|
||||
# ZANO = 16
|
||||
|
||||
|
||||
chainparams = {
|
||||
@@ -154,6 +152,41 @@ chainparams = {
|
||||
'max_amount': 100000 * COIN,
|
||||
}
|
||||
},
|
||||
Coins.DCR: {
|
||||
'name': 'decred',
|
||||
'ticker': 'DCR',
|
||||
'message_magic': 'Decred Signed Message:\n',
|
||||
'blocks_target': 60 * 5,
|
||||
'decimal_places': 8,
|
||||
'mainnet': {
|
||||
'rpcport': 9109,
|
||||
'pubkey_address': 0x073f,
|
||||
'script_address': 0x071a,
|
||||
'key_prefix': 0x22de,
|
||||
'bip44': 42,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
},
|
||||
'testnet': {
|
||||
'rpcport': 19109,
|
||||
'pubkey_address': 0x0f21,
|
||||
'script_address': 0x0efc,
|
||||
'key_prefix': 0x230e,
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
'name': 'testnet3',
|
||||
},
|
||||
'regtest': { # simnet
|
||||
'rpcport': 18656,
|
||||
'pubkey_address': 0x0e91,
|
||||
'script_address': 0x0e6c,
|
||||
'key_prefix': 0x2307,
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
}
|
||||
},
|
||||
Coins.NMC: {
|
||||
'name': 'namecoin',
|
||||
'ticker': 'NMC',
|
||||
@@ -199,18 +232,48 @@ chainparams = {
|
||||
'walletrpcport': 18082,
|
||||
'min_amount': 100000,
|
||||
'max_amount': 10000 * XMR_COIN,
|
||||
'address_prefix': 18,
|
||||
},
|
||||
'testnet': {
|
||||
'rpcport': 28081,
|
||||
'walletrpcport': 28082,
|
||||
'min_amount': 100000,
|
||||
'max_amount': 10000 * XMR_COIN,
|
||||
'address_prefix': 18,
|
||||
},
|
||||
'regtest': {
|
||||
'rpcport': 18081,
|
||||
'walletrpcport': 18082,
|
||||
'min_amount': 100000,
|
||||
'max_amount': 10000 * XMR_COIN,
|
||||
'address_prefix': 18,
|
||||
}
|
||||
},
|
||||
Coins.WOW: {
|
||||
'name': 'wownero',
|
||||
'ticker': 'WOW',
|
||||
'client': 'wow',
|
||||
'decimal_places': 11,
|
||||
'mainnet': {
|
||||
'rpcport': 34568,
|
||||
'walletrpcport': 34572, # todo
|
||||
'min_amount': 100000,
|
||||
'max_amount': 10000 * WOW_COIN,
|
||||
'address_prefix': 4146,
|
||||
},
|
||||
'testnet': {
|
||||
'rpcport': 44568,
|
||||
'walletrpcport': 44572,
|
||||
'min_amount': 100000,
|
||||
'max_amount': 10000 * WOW_COIN,
|
||||
'address_prefix': 4146,
|
||||
},
|
||||
'regtest': {
|
||||
'rpcport': 54568,
|
||||
'walletrpcport': 54572,
|
||||
'min_amount': 100000,
|
||||
'max_amount': 10000 * WOW_COIN,
|
||||
'address_prefix': 4146,
|
||||
}
|
||||
},
|
||||
Coins.PIVX: {
|
||||
@@ -219,6 +282,7 @@ chainparams = {
|
||||
'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,
|
||||
@@ -296,7 +360,8 @@ chainparams = {
|
||||
'message_magic': 'Zcoin Signed Message:\n',
|
||||
'blocks_target': 60 * 10,
|
||||
'decimal_places': 8,
|
||||
'has_csv': True,
|
||||
'has_cltv': False,
|
||||
'has_csv': False,
|
||||
'has_segwit': False,
|
||||
'mainnet': {
|
||||
'rpcport': 8888,
|
||||
@@ -376,90 +441,8 @@ for c, params in chainparams.items():
|
||||
ticker_map[params['ticker'].lower()] = c
|
||||
|
||||
|
||||
def getCoinIdFromTicker(ticker):
|
||||
def getCoinIdFromTicker(ticker: str) -> str:
|
||||
try:
|
||||
return ticker_map[ticker.lower()]
|
||||
except Exception:
|
||||
raise ValueError('Unknown coin')
|
||||
|
||||
|
||||
class CoinInterface:
|
||||
def __init__(self, network):
|
||||
self.setDefaults()
|
||||
self._network = network
|
||||
self._mx_wallet = threading.Lock()
|
||||
|
||||
def setDefaults(self):
|
||||
self._unknown_wallet_seed = True
|
||||
self._restore_height = None
|
||||
|
||||
def make_int(self, amount_in: int, r: int = 0) -> int:
|
||||
return make_int(amount_in, self.exp(), r=r)
|
||||
|
||||
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
|
||||
return format_amount(amount_int, self.exp())
|
||||
|
||||
def coin_name(self) -> str:
|
||||
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) -> str:
|
||||
ticker = chainparams[self.coin_type()]['ticker']
|
||||
if self._network == 'testnet':
|
||||
ticker = 't' + ticker
|
||||
elif self._network == 'regtest':
|
||||
ticker = 'rt' + ticker
|
||||
return ticker
|
||||
|
||||
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']
|
||||
return ticker
|
||||
|
||||
def min_amount(self) -> int:
|
||||
return chainparams[self.coin_type()][self._network]['min_amount']
|
||||
|
||||
def max_amount(self) -> int:
|
||||
return chainparams[self.coin_type()][self._network]['max_amount']
|
||||
|
||||
def setWalletSeedWarning(self, value: bool) -> None:
|
||||
self._unknown_wallet_seed = value
|
||||
|
||||
def setWalletRestoreHeight(self, value: int) -> None:
|
||||
self._restore_height = value
|
||||
|
||||
def knownWalletSeed(self) -> bool:
|
||||
return not self._unknown_wallet_seed
|
||||
|
||||
def chainparams(self):
|
||||
return chainparams[self.coin_type()]
|
||||
|
||||
def chainparams_network(self):
|
||||
return chainparams[self.coin_type()][self._network]
|
||||
|
||||
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):
|
||||
return True
|
||||
str_error: str = str(ex).lower()
|
||||
if 'not enough unlocked money' in str_error:
|
||||
return True
|
||||
if 'no unlocked balance' in str_error:
|
||||
return True
|
||||
if 'transaction was rejected by daemon' in str_error:
|
||||
return True
|
||||
if 'invalid unlocked_balance' in str_error:
|
||||
return True
|
||||
if 'daemon is busy' in str_error:
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
import os
|
||||
|
||||
CONFIG_FILENAME = 'basicswap.json'
|
||||
BASICSWAP_DATADIR = os.getenv('BASICSWAP_DATADIR', '~/.basicswap')
|
||||
BASICSWAP_DATADIR = os.getenv('BASICSWAP_DATADIR', os.path.join('~', '.basicswap'))
|
||||
DEFAULT_ALLOW_CORS = False
|
||||
TEST_DATADIRS = os.path.expanduser(os.getenv('DATADIRS', '/tmp/basicswap'))
|
||||
DEFAULT_TEST_BINDIR = os.path.expanduser(os.getenv('DEFAULT_TEST_BINDIR', '~/.basicswap/bin'))
|
||||
DEFAULT_TEST_BINDIR = os.path.expanduser(os.getenv('DEFAULT_TEST_BINDIR', os.path.join('~', '.basicswap', 'bin')))
|
||||
|
||||
bin_suffix = ('.exe' if os.name == 'nt' else '')
|
||||
PARTICL_BINDIR = os.path.expanduser(os.getenv('PARTICL_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'particl')))
|
||||
|
||||
1
basicswap/contrib/blake256/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
533
basicswap/contrib/blake256/blake256.py
Normal file
@@ -0,0 +1,533 @@
|
||||
|
||||
|
||||
intro = """
|
||||
blake.py
|
||||
version 5, 2-Apr-2014
|
||||
|
||||
BLAKE is a SHA3 round-3 finalist designed and submitted by
|
||||
Jean-Philippe Aumasson et al.
|
||||
|
||||
At the core of BLAKE is a ChaCha-like mixer, very similar
|
||||
to that found in the stream cipher, ChaCha8. Besides being
|
||||
a very good mixer, ChaCha is fast.
|
||||
|
||||
References:
|
||||
http://www.131002.net/blake/
|
||||
http://csrc.nist.gov/groups/ST/hash/sha-3/index.html
|
||||
http://en.wikipedia.org/wiki/BLAKE_(hash_function)
|
||||
|
||||
This implementation assumes all data is in increments of
|
||||
whole bytes. (The formal definition of BLAKE allows for
|
||||
hashing individual bits.) Note too that this implementation
|
||||
does include the round-3 tweaks where the number of rounds
|
||||
was increased to 14/16 from 10/14.
|
||||
|
||||
This version can be imported into both Python2 (2.6 and 2.7)
|
||||
and Python3 programs. Python 2.5 requires an older version
|
||||
of blake.py (version 4).
|
||||
|
||||
Here are some comparative times for different versions of
|
||||
Python:
|
||||
|
||||
64-bit:
|
||||
2.6 6.284s
|
||||
2.7 6.343s
|
||||
3.2 7.620s
|
||||
pypy (2.7) 2.080s
|
||||
|
||||
32-bit:
|
||||
2.5 (32) 15.389s (with psyco)
|
||||
2.7-32 13.645s
|
||||
3.2-32 12.574s
|
||||
|
||||
One test on a 2.0GHz Core 2 Duo of 10,000 iterations of
|
||||
BLAKE-256 on a short message produced a time of 5.7 seconds.
|
||||
Not bad, but if raw speed is what you want, look to the
|
||||
the C version. It is 40x faster and did the same thing
|
||||
in 0.13 seconds.
|
||||
|
||||
Copyright (c) 2009-2012 by Larry Bugbee, Kent, WA
|
||||
ALL RIGHTS RESERVED.
|
||||
|
||||
blake.py IS EXPERIMENTAL SOFTWARE FOR EDUCATIONAL
|
||||
PURPOSES ONLY. IT IS MADE AVAILABLE "AS-IS" WITHOUT
|
||||
WARRANTY OR GUARANTEE OF ANY KIND. USE SIGNIFIES
|
||||
ACCEPTANCE OF ALL RISK.
|
||||
|
||||
To make your learning and experimentation less cumbersome,
|
||||
blake.py is free for any use.
|
||||
|
||||
Enjoy,
|
||||
|
||||
Larry Bugbee
|
||||
March 2011
|
||||
rev May 2011 - fixed Python version check (tx JP)
|
||||
rev Apr 2012 - fixed an out-of-order bit set in final()
|
||||
- moved self-test to a separate test pgm
|
||||
- this now works with Python2 and Python3
|
||||
rev Apr 2014 - added test and conversion of string input
|
||||
to byte string in update() (tx Soham)
|
||||
- added hexdigest() method.
|
||||
- now support state 3 so only one call to
|
||||
final() per instantiation is allowed. all
|
||||
subsequent calls to final(), digest() or
|
||||
hexdigest() simply return the stored value.
|
||||
|
||||
"""
|
||||
|
||||
import struct
|
||||
from binascii import hexlify, unhexlify
|
||||
|
||||
#---------------------------------------------------------------
|
||||
|
||||
class BLAKE(object):
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# initial values, constants and padding
|
||||
|
||||
# IVx for BLAKE-x
|
||||
|
||||
IV64 = [
|
||||
0x6A09E667F3BCC908, 0xBB67AE8584CAA73B,
|
||||
0x3C6EF372FE94F82B, 0xA54FF53A5F1D36F1,
|
||||
0x510E527FADE682D1, 0x9B05688C2B3E6C1F,
|
||||
0x1F83D9ABFB41BD6B, 0x5BE0CD19137E2179,
|
||||
]
|
||||
|
||||
IV48 = [
|
||||
0xCBBB9D5DC1059ED8, 0x629A292A367CD507,
|
||||
0x9159015A3070DD17, 0x152FECD8F70E5939,
|
||||
0x67332667FFC00B31, 0x8EB44A8768581511,
|
||||
0xDB0C2E0D64F98FA7, 0x47B5481DBEFA4FA4,
|
||||
]
|
||||
|
||||
# note: the values here are the same as the high-order
|
||||
# half-words of IV64
|
||||
IV32 = [
|
||||
0x6A09E667, 0xBB67AE85,
|
||||
0x3C6EF372, 0xA54FF53A,
|
||||
0x510E527F, 0x9B05688C,
|
||||
0x1F83D9AB, 0x5BE0CD19,
|
||||
]
|
||||
|
||||
# note: the values here are the same as the low-order
|
||||
# half-words of IV48
|
||||
IV28 = [
|
||||
0xC1059ED8, 0x367CD507,
|
||||
0x3070DD17, 0xF70E5939,
|
||||
0xFFC00B31, 0x68581511,
|
||||
0x64F98FA7, 0xBEFA4FA4,
|
||||
]
|
||||
|
||||
# constants for BLAKE-64 and BLAKE-48
|
||||
C64 = [
|
||||
0x243F6A8885A308D3, 0x13198A2E03707344,
|
||||
0xA4093822299F31D0, 0x082EFA98EC4E6C89,
|
||||
0x452821E638D01377, 0xBE5466CF34E90C6C,
|
||||
0xC0AC29B7C97C50DD, 0x3F84D5B5B5470917,
|
||||
0x9216D5D98979FB1B, 0xD1310BA698DFB5AC,
|
||||
0x2FFD72DBD01ADFB7, 0xB8E1AFED6A267E96,
|
||||
0xBA7C9045F12C7F99, 0x24A19947B3916CF7,
|
||||
0x0801F2E2858EFC16, 0x636920D871574E69,
|
||||
]
|
||||
|
||||
# constants for BLAKE-32 and BLAKE-28
|
||||
# note: concatenate and the values are the same as the values
|
||||
# for the 1st half of C64
|
||||
C32 = [
|
||||
0x243F6A88, 0x85A308D3,
|
||||
0x13198A2E, 0x03707344,
|
||||
0xA4093822, 0x299F31D0,
|
||||
0x082EFA98, 0xEC4E6C89,
|
||||
0x452821E6, 0x38D01377,
|
||||
0xBE5466CF, 0x34E90C6C,
|
||||
0xC0AC29B7, 0xC97C50DD,
|
||||
0x3F84D5B5, 0xB5470917,
|
||||
]
|
||||
|
||||
# the 10 permutations of:0,...15}
|
||||
SIGMA = [
|
||||
[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15],
|
||||
[14,10, 4, 8, 9,15,13, 6, 1,12, 0, 2,11, 7, 5, 3],
|
||||
[11, 8,12, 0, 5, 2,15,13,10,14, 3, 6, 7, 1, 9, 4],
|
||||
[ 7, 9, 3, 1,13,12,11,14, 2, 6, 5,10, 4, 0,15, 8],
|
||||
[ 9, 0, 5, 7, 2, 4,10,15,14, 1,11,12, 6, 8, 3,13],
|
||||
[ 2,12, 6,10, 0,11, 8, 3, 4,13, 7, 5,15,14, 1, 9],
|
||||
[12, 5, 1,15,14,13, 4,10, 0, 7, 6, 3, 9, 2, 8,11],
|
||||
[13,11, 7,14,12, 1, 3, 9, 5, 0,15, 4, 8, 6, 2,10],
|
||||
[ 6,15,14, 9,11, 3, 0, 8,12, 2,13, 7, 1, 4,10, 5],
|
||||
[10, 2, 8, 4, 7, 6, 1, 5,15,11, 9,14, 3,12,13, 0],
|
||||
[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15],
|
||||
[14,10, 4, 8, 9,15,13, 6, 1,12, 0, 2,11, 7, 5, 3],
|
||||
[11, 8,12, 0, 5, 2,15,13,10,14, 3, 6, 7, 1, 9, 4],
|
||||
[ 7, 9, 3, 1,13,12,11,14, 2, 6, 5,10, 4, 0,15, 8],
|
||||
[ 9, 0, 5, 7, 2, 4,10,15,14, 1,11,12, 6, 8, 3,13],
|
||||
[ 2,12, 6,10, 0,11, 8, 3, 4,13, 7, 5,15,14, 1, 9],
|
||||
[12, 5, 1,15,14,13, 4,10, 0, 7, 6, 3, 9, 2, 8,11],
|
||||
[13,11, 7,14,12, 1, 3, 9, 5, 0,15, 4, 8, 6, 2,10],
|
||||
[ 6,15,14, 9,11, 3, 0, 8,12, 2,13, 7, 1, 4,10, 5],
|
||||
[10, 2, 8, 4, 7, 6, 1, 5,15,11, 9,14, 3,12,13, 0],
|
||||
]
|
||||
|
||||
MASK32BITS = 0xFFFFFFFF
|
||||
MASK64BITS = 0xFFFFFFFFFFFFFFFF
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
def __init__(self, hashbitlen):
|
||||
"""
|
||||
load the hashSate structure (copy hashbitlen...)
|
||||
hashbitlen: length of the hash output
|
||||
"""
|
||||
if hashbitlen not in [224, 256, 384, 512]:
|
||||
raise Exception('hash length not 224, 256, 384 or 512')
|
||||
|
||||
self.hashbitlen = hashbitlen
|
||||
self.h = [0]*8 # current chain value (initialized to the IV)
|
||||
self.t = 0 # number of *BITS* hashed so far
|
||||
self.cache = b'' # cached leftover data not yet compressed
|
||||
self.salt = [0]*4 # salt (null by default)
|
||||
self.state = 1 # set to 2 by update and 3 by final
|
||||
self.nullt = 0 # Boolean value for special case \ell_i=0
|
||||
|
||||
# The algorithm is the same for both the 32- and 64- versions
|
||||
# of BLAKE. The difference is in word size (4 vs 8 bytes),
|
||||
# blocksize (64 vs 128 bytes), number of rounds (14 vs 16)
|
||||
# and a few very specific constants.
|
||||
if (hashbitlen == 224) or (hashbitlen == 256):
|
||||
# setup for 32-bit words and 64-bit block
|
||||
self.byte2int = self._fourByte2int
|
||||
self.int2byte = self._int2fourByte
|
||||
self.MASK = self.MASK32BITS
|
||||
self.WORDBYTES = 4
|
||||
self.WORDBITS = 32
|
||||
self.BLKBYTES = 64
|
||||
self.BLKBITS = 512
|
||||
self.ROUNDS = 14 # was 10 before round 3
|
||||
self.cxx = self.C32
|
||||
self.rot1 = 16 # num bits to shift in G
|
||||
self.rot2 = 12 # num bits to shift in G
|
||||
self.rot3 = 8 # num bits to shift in G
|
||||
self.rot4 = 7 # num bits to shift in G
|
||||
self.mul = 0 # for 32-bit words, 32<<self.mul where self.mul = 0
|
||||
|
||||
# 224- and 256-bit versions (32-bit words)
|
||||
if hashbitlen == 224:
|
||||
self.h = self.IV28[:]
|
||||
else:
|
||||
self.h = self.IV32[:]
|
||||
|
||||
elif (hashbitlen == 384) or (hashbitlen == 512):
|
||||
# setup for 64-bit words and 128-bit block
|
||||
self.byte2int = self._eightByte2int
|
||||
self.int2byte = self._int2eightByte
|
||||
self.MASK = self.MASK64BITS
|
||||
self.WORDBYTES = 8
|
||||
self.WORDBITS = 64
|
||||
self.BLKBYTES = 128
|
||||
self.BLKBITS = 1024
|
||||
self.ROUNDS = 16 # was 14 before round 3
|
||||
self.cxx = self.C64
|
||||
self.rot1 = 32 # num bits to shift in G
|
||||
self.rot2 = 25 # num bits to shift in G
|
||||
self.rot3 = 16 # num bits to shift in G
|
||||
self.rot4 = 11 # num bits to shift in G
|
||||
self.mul = 1 # for 64-bit words, 32<<self.mul where self.mul = 1
|
||||
|
||||
# 384- and 512-bit versions (64-bit words)
|
||||
if hashbitlen == 384:
|
||||
self.h = self.IV48[:]
|
||||
else:
|
||||
self.h = self.IV64[:]
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
def _compress(self, block):
|
||||
byte2int = self.byte2int
|
||||
mul = self.mul # de-reference these for ...speed? ;-)
|
||||
cxx = self.cxx
|
||||
rot1 = self.rot1
|
||||
rot2 = self.rot2
|
||||
rot3 = self.rot3
|
||||
rot4 = self.rot4
|
||||
MASK = self.MASK
|
||||
WORDBITS = self.WORDBITS
|
||||
SIGMA = self.SIGMA
|
||||
|
||||
# get message (<<2 is the same as *4 but faster)
|
||||
m = [byte2int(block[i<<2<<mul:(i<<2<<mul)+(4<<mul)]) for i in range(16)]
|
||||
|
||||
# initialization
|
||||
v = [0]*16
|
||||
v[ 0: 8] = [self.h[i] for i in range(8)]
|
||||
v[ 8:16] = [self.cxx[i] for i in range(8)]
|
||||
v[ 8:12] = [v[8+i] ^ self.salt[i] for i in range(4)]
|
||||
if self.nullt == 0: # (i>>1 is the same as i/2 but faster)
|
||||
v[12] = v[12] ^ (self.t & MASK)
|
||||
v[13] = v[13] ^ (self.t & MASK)
|
||||
v[14] = v[14] ^ (self.t >> self.WORDBITS)
|
||||
v[15] = v[15] ^ (self.t >> self.WORDBITS)
|
||||
|
||||
# - - - - - - - - - - - - - - - - -
|
||||
# ready? let's ChaCha!!!
|
||||
|
||||
def G(a, b, c, d, i):
|
||||
va = v[a] # it's faster to deref and reref later
|
||||
vb = v[b]
|
||||
vc = v[c]
|
||||
vd = v[d]
|
||||
|
||||
sri = SIGMA[round][i]
|
||||
sri1 = SIGMA[round][i+1]
|
||||
|
||||
va = ((va + vb) + (m[sri] ^ cxx[sri1]) ) & MASK
|
||||
x = vd ^ va
|
||||
vd = (x >> rot1) | ((x << (WORDBITS-rot1)) & MASK)
|
||||
vc = (vc + vd) & MASK
|
||||
x = vb ^ vc
|
||||
vb = (x >> rot2) | ((x << (WORDBITS-rot2)) & MASK)
|
||||
|
||||
va = ((va + vb) + (m[sri1] ^ cxx[sri]) ) & MASK
|
||||
x = vd ^ va
|
||||
vd = (x >> rot3) | ((x << (WORDBITS-rot3)) & MASK)
|
||||
vc = (vc + vd) & MASK
|
||||
x = vb ^ vc
|
||||
vb = (x >> rot4) | ((x << (WORDBITS-rot4)) & MASK)
|
||||
|
||||
v[a] = va
|
||||
v[b] = vb
|
||||
v[c] = vc
|
||||
v[d] = vd
|
||||
|
||||
for round in range(self.ROUNDS):
|
||||
# column step
|
||||
G( 0, 4, 8,12, 0)
|
||||
G( 1, 5, 9,13, 2)
|
||||
G( 2, 6,10,14, 4)
|
||||
G( 3, 7,11,15, 6)
|
||||
# diagonal step
|
||||
G( 0, 5,10,15, 8)
|
||||
G( 1, 6,11,12,10)
|
||||
G( 2, 7, 8,13,12)
|
||||
G( 3, 4, 9,14,14)
|
||||
|
||||
# - - - - - - - - - - - - - - - - -
|
||||
|
||||
# save current hash value (use i&0x3 to get 0,1,2,3,0,1,2,3)
|
||||
self.h = [self.h[i]^v[i]^v[i+8]^self.salt[i&0x3]
|
||||
for i in range(8)]
|
||||
# print 'self.h', [num2hex(h) for h in self.h]
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
def addsalt(self, salt):
|
||||
""" adds a salt to the hash function (OPTIONAL)
|
||||
should be called AFTER Init, and BEFORE update
|
||||
salt: a bytestring, length determined by hashbitlen.
|
||||
if not of sufficient length, the bytestring
|
||||
will be assumed to be a big endian number and
|
||||
prefixed with an appropriate number of null
|
||||
bytes, and if too large, only the low order
|
||||
bytes will be used.
|
||||
|
||||
if hashbitlen=224 or 256, then salt will be 16 bytes
|
||||
if hashbitlen=384 or 512, then salt will be 32 bytes
|
||||
"""
|
||||
# fail if addsalt() was not called at the right time
|
||||
if self.state != 1:
|
||||
raise Exception('addsalt() not called after init() and before update()')
|
||||
# salt size is to be 4x word size
|
||||
saltsize = self.WORDBYTES * 4
|
||||
# if too short, prefix with null bytes. if too long,
|
||||
# truncate high order bytes
|
||||
if len(salt) < saltsize:
|
||||
salt = (chr(0)*(saltsize-len(salt)) + salt)
|
||||
else:
|
||||
salt = salt[-saltsize:]
|
||||
# prep the salt array
|
||||
self.salt[0] = self.byte2int(salt[ : 4<<self.mul])
|
||||
self.salt[1] = self.byte2int(salt[ 4<<self.mul: 8<<self.mul])
|
||||
self.salt[2] = self.byte2int(salt[ 8<<self.mul:12<<self.mul])
|
||||
self.salt[3] = self.byte2int(salt[12<<self.mul: ])
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
def update(self, data):
|
||||
""" update the state with new data, storing excess data
|
||||
as necessary. may be called multiple times and if a
|
||||
call sends less than a full block in size, the leftover
|
||||
is cached and will be consumed in the next call
|
||||
data: data to be hashed (bytestring)
|
||||
"""
|
||||
self.state = 2
|
||||
|
||||
BLKBYTES = self.BLKBYTES # de-referenced for improved readability
|
||||
BLKBITS = self.BLKBITS
|
||||
|
||||
datalen = len(data)
|
||||
if not datalen: return
|
||||
|
||||
if type(data) == type(u''):
|
||||
|
||||
# use either of the next two lines for a proper
|
||||
# response under both Python2 and Python3
|
||||
data = data.encode('UTF-8') # converts to byte string
|
||||
#data = bytearray(data, 'utf-8') # use if want mutable
|
||||
|
||||
# This next line works for Py3 but fails under
|
||||
# Py2 because the Py2 version of bytes() will
|
||||
# accept only *one* argument. Arrrrgh!!!
|
||||
#data = bytes(data, 'utf-8') # converts to immutable byte
|
||||
# string but... under p7
|
||||
# bytes() wants only 1 arg
|
||||
# ...a dummy, 2nd argument like encoding=None
|
||||
# that does nothing would at least allow
|
||||
# compatibility between Python2 and Python3.
|
||||
|
||||
left = len(self.cache)
|
||||
fill = BLKBYTES - left
|
||||
|
||||
# if any cached data and any added new data will fill a
|
||||
# full block, fill and compress
|
||||
if left and datalen >= fill:
|
||||
self.cache = self.cache + data[:fill]
|
||||
self.t += BLKBITS # update counter
|
||||
self._compress(self.cache)
|
||||
self.cache = b''
|
||||
data = data[fill:]
|
||||
datalen -= fill
|
||||
|
||||
# compress new data until not enough for a full block
|
||||
while datalen >= BLKBYTES:
|
||||
self.t += BLKBITS # update counter
|
||||
self._compress(data[:BLKBYTES])
|
||||
data = data[BLKBYTES:]
|
||||
datalen -= BLKBYTES
|
||||
|
||||
# cache all leftover bytes until next call to update()
|
||||
if datalen > 0:
|
||||
self.cache = self.cache + data[:datalen]
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
def final(self, data=''):
|
||||
""" finalize the hash -- pad and hash remaining data
|
||||
returns hashval, the digest
|
||||
"""
|
||||
if self.state == 3:
|
||||
# we have already finalized so simply return the
|
||||
# previously calculated/stored hash value
|
||||
return self.hash
|
||||
|
||||
if data:
|
||||
self.update(data)
|
||||
|
||||
ZZ = b'\x00'
|
||||
ZO = b'\x01'
|
||||
OZ = b'\x80'
|
||||
OO = b'\x81'
|
||||
PADDING = OZ + ZZ*128 # pre-formatted padding data
|
||||
|
||||
# copy nb. bits hash in total as a 64-bit BE word
|
||||
# copy nb. bits hash in total as a 128-bit BE word
|
||||
tt = self.t + (len(self.cache) << 3)
|
||||
if self.BLKBYTES == 64:
|
||||
msglen = self._int2eightByte(tt)
|
||||
else:
|
||||
low = tt & self.MASK
|
||||
high = tt >> self.WORDBITS
|
||||
msglen = self._int2eightByte(high) + self._int2eightByte(low)
|
||||
|
||||
# size of block without the words at the end that count
|
||||
# the number of bits, 55 or 111.
|
||||
# Note: (((self.WORDBITS/8)*2)+1) equals ((self.WORDBITS>>2)+1)
|
||||
sizewithout = self.BLKBYTES - ((self.WORDBITS>>2)+1)
|
||||
|
||||
if len(self.cache) == sizewithout:
|
||||
# special case of one padding byte
|
||||
self.t -= 8
|
||||
if self.hashbitlen in [224, 384]:
|
||||
self.update(OZ)
|
||||
else:
|
||||
self.update(OO)
|
||||
else:
|
||||
if len(self.cache) < sizewithout:
|
||||
# enough space to fill the block
|
||||
# use t=0 if no remaining data
|
||||
if len(self.cache) == 0:
|
||||
self.nullt=1
|
||||
self.t -= (sizewithout - len(self.cache)) << 3
|
||||
self.update(PADDING[:sizewithout - len(self.cache)])
|
||||
else:
|
||||
# NOT enough space, need 2 compressions
|
||||
# ...add marker, pad with nulls and compress
|
||||
self.t -= (self.BLKBYTES - len(self.cache)) << 3
|
||||
self.update(PADDING[:self.BLKBYTES - len(self.cache)])
|
||||
# ...now pad w/nulls leaving space for marker & bit count
|
||||
self.t -= (sizewithout+1) << 3
|
||||
self.update(PADDING[1:sizewithout+1]) # pad with zeroes
|
||||
|
||||
self.nullt = 1 # raise flag to set t=0 at the next _compress
|
||||
|
||||
# append a marker byte
|
||||
if self.hashbitlen in [224, 384]:
|
||||
self.update(ZZ)
|
||||
else:
|
||||
self.update(ZO)
|
||||
self.t -= 8
|
||||
|
||||
# append the number of bits (long long)
|
||||
self.t -= self.BLKBYTES
|
||||
self.update(msglen)
|
||||
|
||||
hashval = []
|
||||
if self.BLKBYTES == 64:
|
||||
for h in self.h:
|
||||
hashval.append(self._int2fourByte(h))
|
||||
else:
|
||||
for h in self.h:
|
||||
hashval.append(self._int2eightByte(h))
|
||||
|
||||
self.hash = b''.join(hashval)[:self.hashbitlen >> 3]
|
||||
self.state = 3
|
||||
|
||||
return self.hash
|
||||
|
||||
digest = final # may use digest() as a synonym for final()
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
def hexdigest(self, data=''):
|
||||
return hexlify(self.final(data)).decode('UTF-8')
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# utility functions
|
||||
|
||||
def _fourByte2int(self, bytestr): # see also long2byt() below
|
||||
""" convert a 4-byte string to an int (long) """
|
||||
return struct.unpack('!L', bytestr)[0]
|
||||
|
||||
def _eightByte2int(self, bytestr):
|
||||
""" convert a 8-byte string to an int (long long) """
|
||||
return struct.unpack('!Q', bytestr)[0]
|
||||
|
||||
def _int2fourByte(self, x): # see also long2byt() below
|
||||
""" convert a number to a 4-byte string, high order
|
||||
truncation possible (in Python x could be a BIGNUM)
|
||||
"""
|
||||
return struct.pack('!L', x)
|
||||
|
||||
def _int2eightByte(self, x):
|
||||
""" convert a number to a 8-byte string, high order
|
||||
truncation possible (in Python x could be a BIGNUM)
|
||||
"""
|
||||
return struct.pack('!Q', x)
|
||||
|
||||
|
||||
#---------------------------------------------------------------
|
||||
#---------------------------------------------------------------
|
||||
#---------------------------------------------------------------
|
||||
|
||||
|
||||
def blake_hash(data):
|
||||
return BLAKE(256).digest(data)
|
||||
37
basicswap/contrib/blake256/test.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from blake256 import blake_hash
|
||||
|
||||
testVectors = [
|
||||
["716f6e863f744b9ac22c97ec7b76ea5f5908bc5b2f67c61510bfc4751384ea7a", ""],
|
||||
["43234ff894a9c0590d0246cfc574eb781a80958b01d7a2fa1ac73c673ba5e311", "a"],
|
||||
["658c6d9019a1deddbcb3640a066dfd23471553a307ab941fd3e677ba887be329", "ab"],
|
||||
["1833a9fa7cf4086bd5fda73da32e5a1d75b4c3f89d5c436369f9d78bb2da5c28", "abc"],
|
||||
["35282468f3b93c5aaca6408582fced36e578f67671ed0741c332d68ac72d7aa2", "abcd"],
|
||||
["9278d633efce801c6aa62987d7483d50e3c918caed7d46679551eed91fba8904", "abcde"],
|
||||
["7a17ee5e289845adcafaf6ca1b05c4a281b232a71c7083f66c19ba1d1169a8d4", "abcdef"],
|
||||
["ee8c7f94ff805cb2e644643010ea43b0222056420917ec70c3da764175193f8f", "abcdefg"],
|
||||
["7b37c0876d29c5add7800a1823795a82b809fc12f799ff6a4b5e58d52c42b17e", "abcdefgh"],
|
||||
["bdc514bea74ffbb9c3aa6470b08ceb80a88e313ad65e4a01457bbffd0acc86de", "abcdefghi"],
|
||||
["12e3afb9739df8d727e93d853faeafc374cc55aedc937e5a1e66f5843b1d4c2e", "abcdefghij"],
|
||||
["22297d373b751f581944bb26315133f6fda2f0bf60f65db773900f61f81b7e79", "Discard medicine more than two years old."],
|
||||
["4d48d137bc9cf6d21415b805bf33f59320337d85c673998260e03a02a0d760cd", "He who has a shady past knows that nice guys finish last."],
|
||||
["beba299e10f93e17d45663a6dc4b8c9349e4f5b9bac0d7832389c40a1b401e5c", "I wouldn't marry him with a ten foot pole."],
|
||||
["42e082ae7f967781c6cd4e0ceeaeeb19fb2955adbdbaf8c7ec4613ac130071b3", "Free! Free!/A trip/to Mars/for 900/empty jars/Burma Shave"],
|
||||
["207d06b205bfb359df91b48b6fd8aa6e4798b712d1cc5e91a254da9cef8684a3", "The days of the digital watch are numbered. -Tom Stoppard"],
|
||||
["d56eab6927e371e2148b0788779aaf565d30567af2af822b6be3b90db9767a70", "Nepal premier won't resign."],
|
||||
["01020709ca7fd10dc7756ce767d508d7206167d300b7a7ed76838a8547a7898c", "For every action there is an equal and opposite government program."],
|
||||
["5569a6cc6535a66da221d8f6ad25008f28752d0343f3f1d757f1ecc9b1c61536", "His money is twice tainted: 'taint yours and 'taint mine."],
|
||||
["8ff699b5ac7687c82600e89d0ff6cfa87e7179759184386971feb76fbae9975f", "There is no reason for any individual to have a computer in their home. -Ken Olsen, 1977"],
|
||||
["f4b3a7c85a418b15ce330fd41ae0254b036ad48dd98aa37f0506a995ba9c6029", "It's a tiny change to the code and not completely disgusting. - Bob Manchek"],
|
||||
["1ed94bab64fe560ef0983165fcb067e9a8a971c1db8e6fb151ff9a7c7fe877e3", "size: a.out: bad magic"],
|
||||
["ff15b54992eedf9889f7b4bbb16692881aa01ed10dfc860fdb04785d8185cd3c", "The major problem is with sendmail. -Mark Horton"],
|
||||
["8a0a7c417a47deec0b6474d8c247da142d2e315113a2817af3de8f45690d8652", "Give me a rock, paper and scissors and I will move the world. CCFestoon"],
|
||||
["310d263fdab056a930324cdea5f46f9ea70219c1a74b01009994484113222a62", "If the enemy is within range, then so are you."],
|
||||
["1aaa0903aa4cf872fe494c322a6e535698ea2140e15f26fb6088287aedceb6ba", "It's well we cannot hear the screams/That we create in others' dreams."],
|
||||
["2eb81bcaa9e9185a7587a1b26299dcfb30f2a58a7f29adb584b969725457ad4f", "You remind me of a TV show, but that's all right: I watch it anyway."],
|
||||
["c27b1683ef76e274680ab5492e592997b0d9d5ac5a5f4651b6036f64215256af", "C is as portable as Stonehedge!!"],
|
||||
["3995cce8f32b174c22ffac916124bd095c80205d9d5f1bb08a155ac24b40d6cb", "Even if I could be Shakespeare, I think I should still choose to be Faraday. - A. Huxley"],
|
||||
["496f7063f8bd479bf54e9d87e9ba53e277839ac7fdaecc5105f2879b58ee562f", "The fugacity of a constituent in a mixture of gases at a given temperature is proportional to its mole fraction. Lewis-Randall Rule"],
|
||||
["2e0eff918940b01eea9539a02212f33ee84f77fab201f4287aa6167e4a1ed043", "How can you write a big system without C++? -Paul Glick"]]
|
||||
|
||||
for vectorSet in testVectors:
|
||||
assert vectorSet[0] == blake_hash(vectorSet[1]).encode('hex')
|
||||
3
basicswap/contrib/mnemonic/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .mnemonic import Mnemonic
|
||||
|
||||
__all__ = ["Mnemonic"]
|
||||
298
basicswap/contrib/mnemonic/mnemonic.py
Normal file
@@ -0,0 +1,298 @@
|
||||
#
|
||||
# Copyright (c) 2013 Pavol Rusnak
|
||||
# Copyright (c) 2017 mruddy
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
# this software and associated documentation files (the "Software"), to deal in
|
||||
# the Software without restriction, including without limitation the rights to
|
||||
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
# of the Software, and to permit persons to whom the Software is furnished to do
|
||||
# so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import itertools
|
||||
import os
|
||||
import secrets
|
||||
import typing as t
|
||||
import unicodedata
|
||||
|
||||
PBKDF2_ROUNDS = 2048
|
||||
|
||||
|
||||
class ConfigurationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# Refactored code segments from <https://github.com/keis/base58>
|
||||
def b58encode(v: bytes) -> str:
|
||||
alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
||||
|
||||
p, acc = 1, 0
|
||||
for c in reversed(v):
|
||||
acc += p * c
|
||||
p = p << 8
|
||||
|
||||
string = ""
|
||||
while acc:
|
||||
acc, idx = divmod(acc, 58)
|
||||
string = alphabet[idx : idx + 1] + string
|
||||
return string
|
||||
|
||||
|
||||
class Mnemonic(object):
|
||||
def __init__(self, language: str = "english", wordlist: list[str] | None = None):
|
||||
self.radix = 2048
|
||||
self.language = language
|
||||
|
||||
if wordlist is None:
|
||||
d = os.path.join(os.path.dirname(__file__), f"wordlist/{language}.txt")
|
||||
if os.path.exists(d) and os.path.isfile(d):
|
||||
with open(d, "r", encoding="utf-8") as f:
|
||||
wordlist = [w.strip() for w in f.readlines()]
|
||||
else:
|
||||
raise ConfigurationError("Language not detected")
|
||||
|
||||
if len(wordlist) != self.radix:
|
||||
raise ConfigurationError(f"Wordlist must contain {self.radix} words.")
|
||||
|
||||
self.wordlist = wordlist
|
||||
# Japanese must be joined by ideographic space
|
||||
self.delimiter = "\u3000" if language == "japanese" else " "
|
||||
|
||||
@classmethod
|
||||
def list_languages(cls) -> list[str]:
|
||||
return [
|
||||
f.split(".")[0]
|
||||
for f in os.listdir(os.path.join(os.path.dirname(__file__), "wordlist"))
|
||||
if f.endswith(".txt")
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def normalize_string(txt: t.AnyStr) -> str:
|
||||
if isinstance(txt, bytes):
|
||||
utxt = txt.decode("utf8")
|
||||
elif isinstance(txt, str):
|
||||
utxt = txt
|
||||
else:
|
||||
raise TypeError("String value expected")
|
||||
|
||||
return unicodedata.normalize("NFKD", utxt)
|
||||
|
||||
@classmethod
|
||||
def detect_language(cls, code: str) -> str:
|
||||
"""Scan the Mnemonic until the language becomes unambiguous, including as abbreviation prefixes.
|
||||
|
||||
Unfortunately, there are valid words that are ambiguous between languages, which are complete words
|
||||
in one language and are prefixes in another:
|
||||
|
||||
english: abandon ... about
|
||||
french: abandon ... aboutir
|
||||
|
||||
If prefixes remain ambiguous, require exactly one language where word(s) match exactly.
|
||||
"""
|
||||
code = cls.normalize_string(code)
|
||||
possible = set(cls(lang) for lang in cls.list_languages())
|
||||
words = set(code.split())
|
||||
for word in words:
|
||||
# possible languages have candidate(s) starting with the word/prefix
|
||||
possible = set(
|
||||
p for p in possible if any(c.startswith(word) for c in p.wordlist)
|
||||
)
|
||||
if not possible:
|
||||
raise ConfigurationError(f"Language unrecognized for {word!r}")
|
||||
if len(possible) == 1:
|
||||
return possible.pop().language
|
||||
# Multiple languages match: A prefix in many, but an exact match in one determines language.
|
||||
complete = set()
|
||||
for word in words:
|
||||
exact = set(p for p in possible if word in p.wordlist)
|
||||
if len(exact) == 1:
|
||||
complete.update(exact)
|
||||
if len(complete) == 1:
|
||||
return complete.pop().language
|
||||
raise ConfigurationError(
|
||||
f"Language ambiguous between {', '.join(p.language for p in possible)}"
|
||||
)
|
||||
|
||||
def generate(self, strength: int = 128) -> str:
|
||||
"""
|
||||
Create a new mnemonic using a random generated number as entropy.
|
||||
|
||||
As defined in BIP39, the entropy must be a multiple of 32 bits, and its size must be between 128 and 256 bits.
|
||||
Therefore the possible values for `strength` are 128, 160, 192, 224 and 256.
|
||||
|
||||
If not provided, the default entropy length will be set to 128 bits.
|
||||
|
||||
The return is a list of words that encodes the generated entropy.
|
||||
|
||||
:param strength: Number of bytes used as entropy
|
||||
:type strength: int
|
||||
:return: A randomly generated mnemonic
|
||||
:rtype: str
|
||||
"""
|
||||
if strength not in [128, 160, 192, 224, 256]:
|
||||
raise ValueError(
|
||||
"Invalid strength value. Allowed values are [128, 160, 192, 224, 256]."
|
||||
)
|
||||
return self.to_mnemonic(secrets.token_bytes(strength // 8))
|
||||
|
||||
# Adapted from <http://tinyurl.com/oxmn476>
|
||||
def to_entropy(self, words: list[str] | str) -> bytearray:
|
||||
if not isinstance(words, list):
|
||||
words = words.split(" ")
|
||||
if len(words) not in [12, 15, 18, 21, 24]:
|
||||
raise ValueError(
|
||||
"Number of words must be one of the following: [12, 15, 18, 21, 24], but it is not (%d)."
|
||||
% len(words)
|
||||
)
|
||||
# Look up all the words in the list and construct the
|
||||
# concatenation of the original entropy and the checksum.
|
||||
concatLenBits = len(words) * 11
|
||||
concatBits = [False] * concatLenBits
|
||||
wordindex = 0
|
||||
for word in words:
|
||||
# Find the words index in the wordlist
|
||||
ndx = self.wordlist.index(self.normalize_string(word))
|
||||
if ndx < 0:
|
||||
raise LookupError('Unable to find "%s" in word list.' % word)
|
||||
# Set the next 11 bits to the value of the index.
|
||||
for ii in range(11):
|
||||
concatBits[(wordindex * 11) + ii] = (ndx & (1 << (10 - ii))) != 0
|
||||
wordindex += 1
|
||||
checksumLengthBits = concatLenBits // 33
|
||||
entropyLengthBits = concatLenBits - checksumLengthBits
|
||||
# Extract original entropy as bytes.
|
||||
entropy = bytearray(entropyLengthBits // 8)
|
||||
for ii in range(len(entropy)):
|
||||
for jj in range(8):
|
||||
if concatBits[(ii * 8) + jj]:
|
||||
entropy[ii] |= 1 << (7 - jj)
|
||||
# Take the digest of the entropy.
|
||||
hashBytes = hashlib.sha256(entropy).digest()
|
||||
hashBits = list(
|
||||
itertools.chain.from_iterable(
|
||||
[c & (1 << (7 - i)) != 0 for i in range(8)] for c in hashBytes
|
||||
)
|
||||
)
|
||||
# Check all the checksum bits.
|
||||
for i in range(checksumLengthBits):
|
||||
if concatBits[entropyLengthBits + i] != hashBits[i]:
|
||||
raise ValueError("Failed checksum.")
|
||||
return entropy
|
||||
|
||||
def to_mnemonic(self, data: bytes) -> str:
|
||||
if len(data) not in [16, 20, 24, 28, 32]:
|
||||
raise ValueError(
|
||||
f"Data length should be one of the following: [16, 20, 24, 28, 32], but it is not {len(data)}."
|
||||
)
|
||||
h = hashlib.sha256(data).hexdigest()
|
||||
b = (
|
||||
bin(int.from_bytes(data, byteorder="big"))[2:].zfill(len(data) * 8)
|
||||
+ bin(int(h, 16))[2:].zfill(256)[: len(data) * 8 // 32]
|
||||
)
|
||||
result = []
|
||||
for i in range(len(b) // 11):
|
||||
idx = int(b[i * 11 : (i + 1) * 11], 2)
|
||||
result.append(self.wordlist[idx])
|
||||
return self.delimiter.join(result)
|
||||
|
||||
def check(self, mnemonic: str) -> bool:
|
||||
mnemonic_list = self.normalize_string(mnemonic).split(" ")
|
||||
# list of valid mnemonic lengths
|
||||
if len(mnemonic_list) not in [12, 15, 18, 21, 24]:
|
||||
return False
|
||||
try:
|
||||
idx = map(
|
||||
lambda x: bin(self.wordlist.index(x))[2:].zfill(11), mnemonic_list
|
||||
)
|
||||
b = "".join(idx)
|
||||
except ValueError:
|
||||
return False
|
||||
l = len(b) # noqa: E741
|
||||
d = b[: l // 33 * 32]
|
||||
h = b[-l // 33 :]
|
||||
nd = int(d, 2).to_bytes(l // 33 * 4, byteorder="big")
|
||||
nh = bin(int(hashlib.sha256(nd).hexdigest(), 16))[2:].zfill(256)[: l // 33]
|
||||
return h == nh
|
||||
|
||||
def expand_word(self, prefix: str) -> str:
|
||||
if prefix in self.wordlist:
|
||||
return prefix
|
||||
else:
|
||||
matches = [word for word in self.wordlist if word.startswith(prefix)]
|
||||
if len(matches) == 1: # matched exactly one word in the wordlist
|
||||
return matches[0]
|
||||
else:
|
||||
# exact match not found.
|
||||
# this is not a validation routine, just return the input
|
||||
return prefix
|
||||
|
||||
def expand(self, mnemonic: str) -> str:
|
||||
return " ".join(map(self.expand_word, mnemonic.split(" ")))
|
||||
|
||||
@classmethod
|
||||
def to_seed(cls, mnemonic: str, passphrase: str = "") -> bytes:
|
||||
mnemonic = cls.normalize_string(mnemonic)
|
||||
passphrase = cls.normalize_string(passphrase)
|
||||
passphrase = "mnemonic" + passphrase
|
||||
mnemonic_bytes = mnemonic.encode("utf-8")
|
||||
passphrase_bytes = passphrase.encode("utf-8")
|
||||
stretched = hashlib.pbkdf2_hmac(
|
||||
"sha512", mnemonic_bytes, passphrase_bytes, PBKDF2_ROUNDS
|
||||
)
|
||||
return stretched[:64]
|
||||
|
||||
@staticmethod
|
||||
def to_hd_master_key(seed: bytes, testnet: bool = False) -> str:
|
||||
if len(seed) != 64:
|
||||
raise ValueError("Provided seed should have length of 64")
|
||||
|
||||
# Compute HMAC-SHA512 of seed
|
||||
seed = hmac.new(b"Bitcoin seed", seed, digestmod=hashlib.sha512).digest()
|
||||
|
||||
# Serialization format can be found at: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#serialization-format
|
||||
xprv = b"\x04\x88\xad\xe4" # Version for private mainnet
|
||||
if testnet:
|
||||
xprv = b"\x04\x35\x83\x94" # Version for private testnet
|
||||
xprv += b"\x00" * 9 # Depth, parent fingerprint, and child number
|
||||
xprv += seed[32:] # Chain code
|
||||
xprv += b"\x00" + seed[:32] # Master key
|
||||
|
||||
# Double hash using SHA256
|
||||
hashed_xprv = hashlib.sha256(xprv).digest()
|
||||
hashed_xprv = hashlib.sha256(hashed_xprv).digest()
|
||||
|
||||
# Append 4 bytes of checksum
|
||||
xprv += hashed_xprv[:4]
|
||||
|
||||
# Return base58
|
||||
return b58encode(xprv)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
import sys
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
hex_data = sys.argv[1]
|
||||
else:
|
||||
hex_data = sys.stdin.readline().strip()
|
||||
data = bytes.fromhex(hex_data)
|
||||
m = Mnemonic("english")
|
||||
print(m.to_mnemonic(data))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1
basicswap/contrib/mnemonic/py.typed
Normal file
@@ -0,0 +1 @@
|
||||
# Marker file for PEP 561.
|
||||
2048
basicswap/contrib/mnemonic/wordlist/chinese_simplified.txt
Normal file
2048
basicswap/contrib/mnemonic/wordlist/chinese_traditional.txt
Normal file
2048
basicswap/contrib/mnemonic/wordlist/czech.txt
Normal file
2048
basicswap/contrib/mnemonic/wordlist/english.txt
Normal file
2048
basicswap/contrib/mnemonic/wordlist/french.txt
Normal file
2048
basicswap/contrib/mnemonic/wordlist/italian.txt
Normal file
2048
basicswap/contrib/mnemonic/wordlist/japanese.txt
Normal file
2048
basicswap/contrib/mnemonic/wordlist/korean.txt
Normal file
2048
basicswap/contrib/mnemonic/wordlist/portuguese.txt
Normal file
2048
basicswap/contrib/mnemonic/wordlist/russian.txt
Normal file
2048
basicswap/contrib/mnemonic/wordlist/spanish.txt
Normal file
2048
basicswap/contrib/mnemonic/wordlist/turkish.txt
Normal file
@@ -1,18 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019-2023 tecnovert
|
||||
# Copyright (c) 2019-2024 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import time
|
||||
import struct
|
||||
import sqlalchemy as sa
|
||||
|
||||
from enum import IntEnum, auto
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
|
||||
CURRENT_DB_VERSION = 21
|
||||
CURRENT_DB_VERSION = 24
|
||||
CURRENT_DB_DATA_VERSION = 4
|
||||
Base = declarative_base()
|
||||
|
||||
@@ -34,6 +33,10 @@ def strConcepts(state):
|
||||
return 'Unknown'
|
||||
|
||||
|
||||
def pack_state(new_state: int, now: int) -> bytes:
|
||||
return int(new_state).to_bytes(4, 'little') + now.to_bytes(8, 'little')
|
||||
|
||||
|
||||
class DBKVInt(Base):
|
||||
__tablename__ = 'kv_int'
|
||||
|
||||
@@ -58,6 +61,7 @@ class Offer(Base):
|
||||
coin_from = sa.Column(sa.Integer)
|
||||
coin_to = sa.Column(sa.Integer)
|
||||
amount_from = sa.Column(sa.BigInteger)
|
||||
amount_to = sa.Column(sa.BigInteger)
|
||||
rate = sa.Column(sa.BigInteger)
|
||||
min_bid_amount = sa.Column(sa.BigInteger)
|
||||
time_valid = sa.Column(sa.BigInteger)
|
||||
@@ -67,6 +71,7 @@ class Offer(Base):
|
||||
|
||||
proof_address = sa.Column(sa.String)
|
||||
proof_signature = sa.Column(sa.LargeBinary)
|
||||
proof_utxos = sa.Column(sa.LargeBinary)
|
||||
pkhash_seller = sa.Column(sa.LargeBinary)
|
||||
secret_hash = sa.Column(sa.LargeBinary)
|
||||
|
||||
@@ -95,9 +100,9 @@ class Offer(Base):
|
||||
now = int(time.time())
|
||||
self.state = new_state
|
||||
if self.states is None:
|
||||
self.states = struct.pack('<iq', new_state, now)
|
||||
self.states = pack_state(new_state, now)
|
||||
else:
|
||||
self.states += struct.pack('<iq', new_state, now)
|
||||
self.states += pack_state(new_state, now)
|
||||
|
||||
|
||||
class Bid(Base):
|
||||
@@ -115,12 +120,14 @@ class Bid(Base):
|
||||
expire_at = sa.Column(sa.BigInteger)
|
||||
bid_addr = 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
|
||||
|
||||
recovered_secret = sa.Column(sa.LargeBinary)
|
||||
amount_to = sa.Column(sa.BigInteger) # amount * offer.rate
|
||||
|
||||
pkhash_buyer = sa.Column(sa.LargeBinary)
|
||||
pkhash_buyer_to = sa.Column(sa.LargeBinary) # Used for the ptx if coin pubkey hashes differ
|
||||
amount = sa.Column(sa.BigInteger)
|
||||
rate = sa.Column(sa.BigInteger)
|
||||
|
||||
@@ -181,9 +188,14 @@ class Bid(Base):
|
||||
if state_note is not None:
|
||||
self.state_note = state_note
|
||||
if self.states is None:
|
||||
self.states = struct.pack('<iq', new_state, now)
|
||||
self.states = pack_state(new_state, now)
|
||||
else:
|
||||
self.states += struct.pack('<iq', new_state, now)
|
||||
self.states += pack_state(new_state, now)
|
||||
|
||||
def getLockTXBVout(self):
|
||||
if self.xmr_b_lock_tx:
|
||||
return self.xmr_b_lock_tx.vout
|
||||
return None
|
||||
|
||||
|
||||
class SwapTx(Base):
|
||||
@@ -220,7 +232,11 @@ class SwapTx(Base):
|
||||
if self.state == new_state:
|
||||
return
|
||||
self.state = new_state
|
||||
self.states = (self.states if self.states is not None else bytes()) + struct.pack('<iq', new_state, int(time.time()))
|
||||
now: int = int(time.time())
|
||||
if self.states is None:
|
||||
self.states = pack_state(new_state, now)
|
||||
else:
|
||||
self.states += pack_state(new_state, now)
|
||||
|
||||
|
||||
class PrefundedTx(Base):
|
||||
@@ -512,3 +528,14 @@ class MessageLink(Base):
|
||||
msg_type = sa.Column(sa.Integer)
|
||||
msg_sequence = sa.Column(sa.Integer)
|
||||
msg_id = sa.Column(sa.LargeBinary)
|
||||
|
||||
|
||||
class CheckedBlock(Base):
|
||||
__tablename__ = 'checkedblocks'
|
||||
|
||||
record_id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
|
||||
created_at = sa.Column(sa.BigInteger)
|
||||
coin_type = sa.Column(sa.Integer)
|
||||
block_height = sa.Column(sa.Integer)
|
||||
block_hash = sa.Column(sa.LargeBinary)
|
||||
block_time = sa.Column(sa.BigInteger)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2022-2023 tecnovert
|
||||
# Copyright (c) 2022-2024 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -93,7 +93,7 @@ def upgradeDatabaseData(self, data_version):
|
||||
created_at=now))
|
||||
|
||||
self.db_data_version = CURRENT_DB_DATA_VERSION
|
||||
self.setIntKVInSession('db_data_version', self.db_data_version, session)
|
||||
self.setIntKV('db_data_version', self.db_data_version, session)
|
||||
session.commit()
|
||||
self.log.info('Upgraded database records to version {}'.format(self.db_data_version))
|
||||
finally:
|
||||
@@ -293,10 +293,28 @@ def upgradeDatabase(self, db_version):
|
||||
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')
|
||||
elif current_version == 22:
|
||||
db_version += 1
|
||||
session.execute('ALTER TABLE offers ADD COLUMN amount_to INTEGER')
|
||||
elif current_version == 23:
|
||||
db_version += 1
|
||||
session.execute('''
|
||||
CREATE TABLE checkedblocks (
|
||||
record_id INTEGER NOT NULL,
|
||||
created_at BIGINT,
|
||||
coin_type INTEGER,
|
||||
block_height INTEGER,
|
||||
block_hash BLOB,
|
||||
block_time INTEGER,
|
||||
PRIMARY KEY (record_id))''')
|
||||
session.execute('ALTER TABLE bids ADD COLUMN pkhash_buyer_to BLOB')
|
||||
if current_version != db_version:
|
||||
self.db_version = db_version
|
||||
self.setIntKVInSession('db_version', db_version, session)
|
||||
self.setIntKV('db_version', db_version, session)
|
||||
session.commit()
|
||||
session.close()
|
||||
session.remove()
|
||||
|
||||
@@ -52,5 +52,7 @@ def remove_expired_data(self, time_offset: int = 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 ''))
|
||||
|
||||
session.execute('DELETE FROM checkedblocks WHERE created_at <= :expired_at', {'expired_at': now - time_offset})
|
||||
|
||||
finally:
|
||||
self.closeSession(session)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019-2023 tecnovert
|
||||
# Copyright (c) 2019-2024 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -98,6 +98,8 @@ def parse_cmd(cmd: str, type_map: str):
|
||||
type_ind = type_map[i]
|
||||
if type_ind == 'i':
|
||||
typed_params.append(int(param))
|
||||
elif type_ind == 'f':
|
||||
typed_params.append(float(param))
|
||||
elif type_ind == 'b':
|
||||
typed_params.append(toBool(param))
|
||||
elif type_ind == 'j':
|
||||
@@ -109,7 +111,6 @@ def parse_cmd(cmd: str, type_map: str):
|
||||
|
||||
|
||||
class HttpHandler(BaseHTTPRequestHandler):
|
||||
|
||||
def log_error(self, format, *args):
|
||||
super().log_message(format, *args)
|
||||
|
||||
@@ -143,9 +144,12 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
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:
|
||||
tor_state = get_tor_established_state(swap_client)
|
||||
args_dict['tor_established'] = True if tor_state == '1' else False
|
||||
except Exception as e:
|
||||
args_dict['tor_established'] = False
|
||||
if swap_client.debug:
|
||||
swap_client.log.error(f"Error getting Tor state: {str(e)}")
|
||||
swap_client.log.error(traceback.format_exc())
|
||||
|
||||
if swap_client._show_notifications:
|
||||
@@ -265,6 +269,8 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
summary = swap_client.getSummary()
|
||||
|
||||
result = None
|
||||
cmd = ''
|
||||
coin_type_selected = -1
|
||||
coin_type = -1
|
||||
coin_id = -1
|
||||
call_type = 'cli'
|
||||
@@ -277,61 +283,84 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
call_type = get_data_entry_or(form_data, 'call_type', 'cli')
|
||||
type_map = get_data_entry_or(form_data, 'type_map', '')
|
||||
try:
|
||||
coin_id = int(get_data_entry(form_data, 'coin_type'))
|
||||
if coin_id in (-2, -3, -4):
|
||||
coin_type = Coins(Coins.XMR)
|
||||
else:
|
||||
coin_type = Coins(coin_id)
|
||||
coin_type_selected = get_data_entry(form_data, 'coin_type')
|
||||
coin_type_split = coin_type_selected.split(',')
|
||||
coin_type = Coins(int(coin_type_split[0]))
|
||||
coin_variant = int(coin_type_split[1])
|
||||
except Exception:
|
||||
raise ValueError('Unknown Coin Type')
|
||||
|
||||
if coin_type in (Coins.DCR,):
|
||||
call_type = 'http'
|
||||
|
||||
try:
|
||||
cmd = get_data_entry(form_data, 'cmd')
|
||||
except Exception:
|
||||
raise ValueError('Invalid command')
|
||||
if coin_type == Coins.XMR:
|
||||
if coin_type in (Coins.XMR, Coins.WOW):
|
||||
ci = swap_client.ci(coin_type)
|
||||
arr = cmd.split(None, 1)
|
||||
method = arr[0]
|
||||
params = json.loads(arr[1]) if len(arr) > 1 else []
|
||||
if coin_id == -4:
|
||||
rv = ci.rpc_wallet_cb(method, params)
|
||||
elif coin_id == -3:
|
||||
rv = ci.rpc_cb(method, params)
|
||||
elif coin_id == -2:
|
||||
if coin_variant == 2:
|
||||
rv = ci.rpc_wallet(method, params)
|
||||
elif coin_variant == 0:
|
||||
rv = ci.rpc(method, params)
|
||||
elif coin_variant == 1:
|
||||
if params == []:
|
||||
params = None
|
||||
rv = ci.rpc_cb2(method, params)
|
||||
rv = ci.rpc2(method, params)
|
||||
else:
|
||||
raise ValueError('Unknown XMR RPC variant')
|
||||
raise ValueError('Unknown RPC variant')
|
||||
result = json.dumps(rv, indent=4)
|
||||
else:
|
||||
if call_type == 'http':
|
||||
ci = swap_client.ci(coin_type)
|
||||
method, params = parse_cmd(cmd, type_map)
|
||||
rv = swap_client.ci(coin_type).rpc_callback(method, params)
|
||||
if coin_variant == 1:
|
||||
rv = ci.rpc_wallet(method, params)
|
||||
elif coin_variant == 2:
|
||||
rv = ci.rpc_wallet_mweb(method, params)
|
||||
else:
|
||||
if coin_type in (Coins.DCR, ):
|
||||
rv = ci.rpc(method, params)
|
||||
else:
|
||||
rv = ci.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:
|
||||
result = str(ex)
|
||||
result = cmd + '\n' + str(ex)
|
||||
if self.server.swap_client.debug is True:
|
||||
self.server.swap_client.log.error(traceback.format_exc())
|
||||
|
||||
template = env.get_template('rpc.html')
|
||||
|
||||
coins = listAvailableCoins(swap_client, with_variants=False)
|
||||
coins = [c for c in coins if c[0] != Coins.XMR]
|
||||
coins.append((-2, 'Monero'))
|
||||
coins.append((-3, 'Monero JSON'))
|
||||
coins.append((-4, 'Monero Wallet'))
|
||||
coin_available = listAvailableCoins(swap_client, with_variants=False)
|
||||
with_xmr: bool = any(c[0] == Coins.XMR for c in coin_available)
|
||||
with_wow: bool = any(c[0] == Coins.WOW for c in coin_available)
|
||||
coins = [(str(c[0]) + ',0', c[1]) for c in coin_available if c[0] not in (Coins.XMR, Coins.WOW)]
|
||||
|
||||
if any(c[0] == Coins.DCR for c in coin_available):
|
||||
coins.append((str(int(Coins.DCR)) + ',1', 'Decred Wallet'))
|
||||
if any(c[0] == Coins.LTC for c in coin_available):
|
||||
coins.append((str(int(Coins.LTC)) + ',2', 'Litecoin MWEB Wallet'))
|
||||
if with_xmr:
|
||||
coins.append((str(int(Coins.XMR)) + ',0', 'Monero'))
|
||||
coins.append((str(int(Coins.XMR)) + ',1', 'Monero JSON'))
|
||||
coins.append((str(int(Coins.XMR)) + ',2', 'Monero Wallet'))
|
||||
if with_wow:
|
||||
coins.append((str(int(Coins.WOW)) + ',0', 'Wownero'))
|
||||
coins.append((str(int(Coins.WOW)) + ',1', 'Wownero JSON'))
|
||||
coins.append((str(int(Coins.WOW)) + ',2', 'Wownero Wallet'))
|
||||
|
||||
return self.render_template(template, {
|
||||
'messages': messages,
|
||||
'err_messages': err_messages,
|
||||
'coins': coins,
|
||||
'coin_type': coin_id,
|
||||
'coin_type': coin_type_selected,
|
||||
'call_type': call_type,
|
||||
'result': result,
|
||||
'messages': messages,
|
||||
@@ -382,12 +411,10 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
summary = swap_client.getSummary()
|
||||
template = env.get_template('index.html')
|
||||
return self.render_template(template, {
|
||||
'refresh': 30,
|
||||
'summary': summary,
|
||||
'use_tor_proxy': swap_client.use_tor_proxy
|
||||
})
|
||||
self.send_response(302)
|
||||
self.send_header('Location', '/offers')
|
||||
self.end_headers()
|
||||
return b''
|
||||
|
||||
def page_404(self, url_split):
|
||||
swap_client = self.server.swap_client
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/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
|
||||
|
||||
238
basicswap/interface/base.py
Normal file
@@ -0,0 +1,238 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2024 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import threading
|
||||
|
||||
from enum import IntEnum
|
||||
|
||||
from basicswap.chainparams import (
|
||||
chainparams,
|
||||
)
|
||||
from basicswap.util import (
|
||||
ensure,
|
||||
i2b, b2i,
|
||||
make_int,
|
||||
format_amount,
|
||||
TemporaryError,
|
||||
)
|
||||
from basicswap.util.crypto import (
|
||||
hash160,
|
||||
)
|
||||
from basicswap.util.ecc import (
|
||||
ep,
|
||||
getSecretInt,
|
||||
)
|
||||
from coincurve.dleag import (
|
||||
verify_secp256k1_point
|
||||
)
|
||||
from coincurve.keys import (
|
||||
PublicKey,
|
||||
)
|
||||
|
||||
|
||||
class Curves(IntEnum):
|
||||
secp256k1 = 1
|
||||
ed25519 = 2
|
||||
|
||||
|
||||
class CoinInterface:
|
||||
@staticmethod
|
||||
def watch_blocks_for_scripts() -> bool:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def compareFeeRates(a, b) -> bool:
|
||||
return abs(a - b) < 20
|
||||
|
||||
def __init__(self, network):
|
||||
self.setDefaults()
|
||||
self._network = network
|
||||
self._mx_wallet = threading.Lock()
|
||||
|
||||
def setDefaults(self):
|
||||
self._unknown_wallet_seed = True
|
||||
self._restore_height = None
|
||||
|
||||
def make_int(self, amount_in: int, r: int = 0) -> int:
|
||||
return make_int(amount_in, self.exp(), r=r)
|
||||
|
||||
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
|
||||
return format_amount(amount_int, self.exp())
|
||||
|
||||
def coin_name(self) -> str:
|
||||
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) -> str:
|
||||
ticker = chainparams[self.coin_type()]['ticker']
|
||||
if self._network == 'testnet':
|
||||
ticker = 't' + ticker
|
||||
elif self._network == 'regtest':
|
||||
ticker = 'rt' + ticker
|
||||
return ticker
|
||||
|
||||
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']
|
||||
return ticker
|
||||
|
||||
def min_amount(self) -> int:
|
||||
return chainparams[self.coin_type()][self._network]['min_amount']
|
||||
|
||||
def max_amount(self) -> int:
|
||||
return chainparams[self.coin_type()][self._network]['max_amount']
|
||||
|
||||
def setWalletSeedWarning(self, value: bool) -> None:
|
||||
self._unknown_wallet_seed = value
|
||||
|
||||
def setWalletRestoreHeight(self, value: int) -> None:
|
||||
self._restore_height = value
|
||||
|
||||
def knownWalletSeed(self) -> bool:
|
||||
return not self._unknown_wallet_seed
|
||||
|
||||
def chainparams(self):
|
||||
return chainparams[self.coin_type()]
|
||||
|
||||
def chainparams_network(self):
|
||||
return chainparams[self.coin_type()][self._network]
|
||||
|
||||
def has_segwit(self) -> bool:
|
||||
return chainparams[self.coin_type()].get('has_segwit', True)
|
||||
|
||||
def use_p2shp2wsh(self) -> bool:
|
||||
# p2sh-p2wsh
|
||||
return False
|
||||
|
||||
def is_transient_error(self, ex) -> bool:
|
||||
if isinstance(ex, TemporaryError):
|
||||
return True
|
||||
str_error: str = str(ex).lower()
|
||||
if 'not enough unlocked money' in str_error:
|
||||
return True
|
||||
if 'no unlocked balance' in str_error:
|
||||
return True
|
||||
if 'transaction was rejected by daemon' in str_error:
|
||||
return True
|
||||
if 'invalid unlocked_balance' in str_error:
|
||||
return True
|
||||
if 'daemon is busy' in str_error:
|
||||
return True
|
||||
if 'timed out' in str_error:
|
||||
return True
|
||||
if 'request-sent' in str_error:
|
||||
return True
|
||||
return False
|
||||
|
||||
def setConfTarget(self, new_conf_target: int) -> None:
|
||||
ensure(new_conf_target >= 1 and new_conf_target < 33, 'Invalid conf_target value')
|
||||
self._conf_target = new_conf_target
|
||||
|
||||
def walletRestoreHeight(self) -> int:
|
||||
return self._restore_height
|
||||
|
||||
def get_connection_type(self):
|
||||
return self._connection_type
|
||||
|
||||
def using_segwit(self) -> bool:
|
||||
# Using btc native segwit
|
||||
return self._use_segwit
|
||||
|
||||
def use_tx_vsize(self) -> bool:
|
||||
return self._use_segwit
|
||||
|
||||
def getLockTxSwapOutputValue(self, bid, xmr_swap) -> int:
|
||||
return bid.amount
|
||||
|
||||
def getLockRefundTxSwapOutputValue(self, bid, xmr_swap) -> int:
|
||||
return xmr_swap.a_swap_refund_value
|
||||
|
||||
def getLockRefundTxSwapOutput(self, xmr_swap) -> int:
|
||||
# Only one prevout exists
|
||||
return 0
|
||||
|
||||
def checkWallets(self) -> int:
|
||||
return 1
|
||||
|
||||
|
||||
class AdaptorSigInterface():
|
||||
def getScriptLockTxDummyWitness(self, script: bytes):
|
||||
return [
|
||||
b'',
|
||||
bytes(72),
|
||||
bytes(72),
|
||||
bytes(len(script))
|
||||
]
|
||||
|
||||
def getScriptLockRefundSpendTxDummyWitness(self, script: bytes):
|
||||
return [
|
||||
b'',
|
||||
bytes(72),
|
||||
bytes(72),
|
||||
bytes((1,)),
|
||||
bytes(len(script))
|
||||
]
|
||||
|
||||
def getScriptLockRefundSwipeTxDummyWitness(self, script: bytes):
|
||||
return [
|
||||
bytes(72),
|
||||
b'',
|
||||
bytes(len(script))
|
||||
]
|
||||
|
||||
|
||||
class Secp256k1Interface(CoinInterface, AdaptorSigInterface):
|
||||
@staticmethod
|
||||
def curve_type():
|
||||
return Curves.secp256k1
|
||||
|
||||
def getNewSecretKey(self) -> bytes:
|
||||
return i2b(getSecretInt())
|
||||
|
||||
def getPubkey(self, privkey: bytes) -> bytes:
|
||||
return PublicKey.from_secret(privkey).format()
|
||||
|
||||
def pkh(self, pubkey: bytes) -> bytes:
|
||||
return hash160(pubkey)
|
||||
|
||||
def verifyKey(self, k: bytes) -> bool:
|
||||
i = b2i(k)
|
||||
return (i < ep.o and i > 0)
|
||||
|
||||
def verifyPubkey(self, pubkey_bytes: bytes) -> bool:
|
||||
return verify_secp256k1_point(pubkey_bytes)
|
||||
|
||||
def isValidAddressHash(self, address_hash: bytes) -> bool:
|
||||
hash_len = len(address_hash)
|
||||
if hash_len == 20:
|
||||
return True
|
||||
|
||||
def isValidPubkey(self, pubkey: bytes) -> bool:
|
||||
try:
|
||||
self.verifyPubkey(pubkey)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def verifySig(self, pubkey: bytes, signed_hash: bytes, sig: bytes) -> bool:
|
||||
pubkey = PublicKey(pubkey)
|
||||
return pubkey.verify(sig, signed_hash, hasher=None)
|
||||
|
||||
def sumKeys(self, ka: bytes, kb: bytes) -> bytes:
|
||||
# TODO: Add to coincurve
|
||||
return i2b((b2i(ka) + b2i(kb)) % ep.o)
|
||||
|
||||
def sumPubkeys(self, Ka: bytes, Kb: bytes) -> bytes:
|
||||
return PublicKey.combine_keys([PublicKey(Ka), PublicKey(Kb)]).format()
|
||||
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"]
|
||||
@@ -68,9 +68,6 @@ mininode_lock = RLock()
|
||||
def sha256(s):
|
||||
return hashlib.new('sha256', s).digest()
|
||||
|
||||
def ripemd160(s):
|
||||
return hashlib.new('ripemd160', s).digest()
|
||||
|
||||
def hash256(s):
|
||||
return sha256(sha256(s))
|
||||
|
||||
@@ -475,10 +472,11 @@ class CTransaction(object):
|
||||
if flags != 0:
|
||||
self.wit.vtxinwit = [CTxInWitness() for i in range(len(self.vin))]
|
||||
self.wit.deserialize(f)
|
||||
|
||||
self.nLockTime = struct.unpack("<I", f.read(4))[0]
|
||||
self.sha256 = None
|
||||
self.hash = None
|
||||
if self.nVersion > 2:
|
||||
if self.nVersion >= 2:
|
||||
self.strDZeel = deser_string(f)
|
||||
|
||||
def serialize_without_witness(self):
|
||||
|
||||
@@ -37,10 +37,6 @@ MAX_SCRIPT_OPCODES = 201
|
||||
|
||||
OPCODE_NAMES = {}
|
||||
|
||||
def hash160(s):
|
||||
return hashlib.new('ripemd160', sha256(s)).digest()
|
||||
|
||||
|
||||
_opcode_instances = []
|
||||
class CScriptOp(int):
|
||||
"""A single script opcode"""
|
||||
|
||||
@@ -51,9 +51,6 @@ MSG_TYPE_MASK = 0xffffffff >> 2
|
||||
def sha256(s):
|
||||
return hashlib.new('sha256', s).digest()
|
||||
|
||||
def ripemd160(s):
|
||||
return hashlib.new('ripemd160', s).digest()
|
||||
|
||||
def hash256(s):
|
||||
return sha256(sha256(s))
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2022 tecnovert
|
||||
# Copyright (c) 2022-2024 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.mnemonic import Mnemonic
|
||||
from basicswap.contrib.test_framework.script import (
|
||||
CScript,
|
||||
OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG
|
||||
@@ -25,38 +25,51 @@ class DASHInterface(BTCInterface):
|
||||
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_callback('upgradetohd', [words, mnemonic_passphrase, self._wallet_passphrase])
|
||||
self._have_checked_seed = False
|
||||
if self._wallet_passphrase != '':
|
||||
self.unlockWallet(self._wallet_passphrase)
|
||||
self._wallet_v20_compatible = False if not swap_client else swap_client.getChainClientSettings(self.coin_type()).get('wallet_v20_compatible', False)
|
||||
|
||||
def decodeAddress(self, address: str) -> bytes:
|
||||
return decodeAddress(address)[1:]
|
||||
|
||||
def checkExpectedSeed(self, key_hash: str):
|
||||
try:
|
||||
rv = self.rpc_callback('dumphdinfo')
|
||||
def getWalletSeedID(self) -> str:
|
||||
hdseed: str = self.rpc_wallet('dumphdinfo')['hdseed']
|
||||
return self.getSeedHash(bytes.fromhex(hdseed)).hex()
|
||||
|
||||
def entropyToMnemonic(self, key: bytes) -> None:
|
||||
return Mnemonic('english').to_mnemonic(key)
|
||||
|
||||
def initialiseWallet(self, key_bytes: bytes) -> None:
|
||||
self._have_checked_seed = False
|
||||
if self._wallet_v20_compatible:
|
||||
self._log.warning('Generating wallet compatible with v20 seed.')
|
||||
words = self.entropyToMnemonic(key_bytes)
|
||||
mnemonic_passphrase = ''
|
||||
self.rpc_wallet('upgradetohd', [words, mnemonic_passphrase, self._wallet_passphrase])
|
||||
self._have_checked_seed = False
|
||||
if self._wallet_passphrase != '':
|
||||
self.unlockWallet(self._wallet_passphrase)
|
||||
return
|
||||
|
||||
key_wif = self.encodeKey(key_bytes)
|
||||
self.rpc_wallet('sethdseed', [True, key_wif])
|
||||
|
||||
def checkExpectedSeed(self, expect_seedid: str) -> bool:
|
||||
self._expect_seedid_hex = expect_seedid
|
||||
rv = self.rpc_wallet('dumphdinfo')
|
||||
if rv['mnemonic'] != '':
|
||||
entropy = Mnemonic('english').to_entropy(rv['mnemonic'].split(' '))
|
||||
entropy_hash = self.getAddressHashFromKey(entropy)[::-1].hex()
|
||||
self._have_checked_seed = True
|
||||
return entropy_hash == key_hash
|
||||
except Exception as e:
|
||||
self._log.warning('checkExpectedSeed failed: {}'.format(str(e)))
|
||||
return False
|
||||
have_expected_seed: bool = expect_seedid == entropy_hash
|
||||
else:
|
||||
have_expected_seed: bool = expect_seedid == self.getWalletSeedID()
|
||||
self._have_checked_seed = True
|
||||
return have_expected_seed
|
||||
|
||||
def withdrawCoin(self, value, addr_to, subfee):
|
||||
params = [addr_to, value, '', '', subfee, False, False, self._conf_target]
|
||||
return self.rpc_callback('sendtoaddress', params)
|
||||
return self.rpc_wallet('sendtoaddress', params)
|
||||
|
||||
def getSpendableBalance(self) -> int:
|
||||
return self.make_int(self.rpc_callback('getwalletinfo')['balance'])
|
||||
return self.make_int(self.rpc_wallet('getwalletinfo')['balance'])
|
||||
|
||||
def getScriptForPubkeyHash(self, pkh: bytes) -> bytearray:
|
||||
# Return P2PKH
|
||||
@@ -66,13 +79,13 @@ class DASHInterface(BTCInterface):
|
||||
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}.')
|
||||
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_callback('gettransaction', [txid_hex])
|
||||
rv = self.rpc_wallet('gettransaction', [txid_hex])
|
||||
except Exception as ex:
|
||||
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
|
||||
return None
|
||||
@@ -84,10 +97,15 @@ class DASHInterface(BTCInterface):
|
||||
|
||||
def unlockWallet(self, password: str):
|
||||
super().unlockWallet(password)
|
||||
# Store password for initialiseWallet
|
||||
self._wallet_passphrase = password
|
||||
if self._wallet_v20_compatible:
|
||||
# Store password for initialiseWallet
|
||||
self._wallet_passphrase = password
|
||||
if not self._have_checked_seed:
|
||||
self._sc.checkWalletSeed(self.coin_type())
|
||||
try:
|
||||
self._sc.checkWalletSeed(self.coin_type())
|
||||
except Exception as ex:
|
||||
# dumphdinfo can fail if the wallet is not initialised
|
||||
self._log.debug(f'DASH checkWalletSeed failed: {ex}.')
|
||||
|
||||
def lockWallet(self):
|
||||
super().lockWallet()
|
||||
|
||||
4
basicswap/interface/dcr/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
from .dcr import DCRInterface
|
||||
|
||||
__all__ = ['DCRInterface',]
|
||||
1452
basicswap/interface/dcr/dcr.py
Normal file
204
basicswap/interface/dcr/messages.py
Normal file
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2024 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import copy
|
||||
from enum import IntEnum
|
||||
from basicswap.util.crypto import blake256
|
||||
from basicswap.util.integer import decode_compactsize, encode_compactsize
|
||||
|
||||
|
||||
class TxSerializeType(IntEnum):
|
||||
Full = 0
|
||||
NoWitness = 1
|
||||
OnlyWitness = 2
|
||||
|
||||
|
||||
class SigHashType(IntEnum):
|
||||
SigHashAll = 0x1
|
||||
SigHashNone = 0x2
|
||||
SigHashSingle = 0x3
|
||||
SigHashAnyOneCanPay = 0x80
|
||||
|
||||
SigHashMask = 0x1f
|
||||
|
||||
|
||||
class SignatureType(IntEnum):
|
||||
STEcdsaSecp256k1 = 0
|
||||
STEd25519 = 1
|
||||
STSchnorrSecp256k1 = 2
|
||||
|
||||
|
||||
class COutPoint:
|
||||
__slots__ = ('hash', 'n', 'tree')
|
||||
|
||||
def __init__(self, hash=0, n=0, tree=0):
|
||||
self.hash = hash
|
||||
self.n = n
|
||||
self.tree = tree
|
||||
|
||||
def get_hash(self) -> bytes:
|
||||
return self.hash.to_bytes(32, 'big')
|
||||
|
||||
|
||||
class CTxIn:
|
||||
__slots__ = ('prevout', 'sequence',
|
||||
'value_in', 'block_height', 'block_index', 'signature_script') # Witness
|
||||
|
||||
def __init__(self, prevout=COutPoint(), sequence=0):
|
||||
self.prevout = prevout
|
||||
self.sequence = sequence
|
||||
self.value_in = -1
|
||||
self.block_height = 0
|
||||
self.block_index = 0xffffffff
|
||||
self.signature_script = bytes()
|
||||
|
||||
|
||||
class CTxOut:
|
||||
__slots__ = ('value', 'version', 'script_pubkey')
|
||||
|
||||
def __init__(self, value=0, script_pubkey=bytes()):
|
||||
self.value = value
|
||||
self.version = 0
|
||||
self.script_pubkey = script_pubkey
|
||||
|
||||
|
||||
class CTransaction:
|
||||
__slots__ = ('hash', 'version', 'vin', 'vout', 'locktime', 'expiry')
|
||||
|
||||
def __init__(self, tx=None):
|
||||
if tx is None:
|
||||
self.version = 1
|
||||
self.vin = []
|
||||
self.vout = []
|
||||
self.locktime = 0
|
||||
self.expiry = 0
|
||||
else:
|
||||
self.version = tx.version
|
||||
self.vin = copy.deepcopy(tx.vin)
|
||||
self.vout = copy.deepcopy(tx.vout)
|
||||
self.locktime = tx.locktime
|
||||
self.expiry = tx.expiry
|
||||
|
||||
def deserialize(self, data: bytes) -> None:
|
||||
|
||||
version = int.from_bytes(data[:4], 'little')
|
||||
self.version = version & 0xffff
|
||||
ser_type: int = version >> 16
|
||||
o = 4
|
||||
|
||||
if ser_type == TxSerializeType.Full or ser_type == TxSerializeType.NoWitness:
|
||||
num_txin, nb = decode_compactsize(data, o)
|
||||
o += nb
|
||||
|
||||
for i in range(num_txin):
|
||||
txi = CTxIn()
|
||||
txi.prevout = COutPoint()
|
||||
txi.prevout.hash = int.from_bytes(data[o:o + 32], 'little')
|
||||
o += 32
|
||||
txi.prevout.n = int.from_bytes(data[o:o + 4], 'little')
|
||||
o += 4
|
||||
txi.prevout.tree = data[o]
|
||||
o += 1
|
||||
txi.sequence = int.from_bytes(data[o:o + 4], 'little')
|
||||
o += 4
|
||||
self.vin.append(txi)
|
||||
|
||||
num_txout, nb = decode_compactsize(data, o)
|
||||
o += nb
|
||||
|
||||
for i in range(num_txout):
|
||||
txo = CTxOut()
|
||||
txo.value = int.from_bytes(data[o:o + 8], 'little')
|
||||
o += 8
|
||||
txo.version = int.from_bytes(data[o:o + 2], 'little')
|
||||
o += 2
|
||||
script_bytes, nb = decode_compactsize(data, o)
|
||||
o += nb
|
||||
txo.script_pubkey = data[o:o + script_bytes]
|
||||
o += script_bytes
|
||||
self.vout.append(txo)
|
||||
|
||||
self.locktime = int.from_bytes(data[o:o + 4], 'little')
|
||||
o += 4
|
||||
self.expiry = int.from_bytes(data[o:o + 4], 'little')
|
||||
o += 4
|
||||
|
||||
if ser_type == TxSerializeType.NoWitness:
|
||||
return
|
||||
|
||||
num_wit_scripts, nb = decode_compactsize(data, o)
|
||||
o += nb
|
||||
|
||||
if ser_type == TxSerializeType.OnlyWitness:
|
||||
self.vin = [CTxIn() for _ in range(num_wit_scripts)]
|
||||
else:
|
||||
if num_wit_scripts != len(self.vin):
|
||||
raise ValueError('non equal witness and prefix txin quantities')
|
||||
|
||||
for i in range(num_wit_scripts):
|
||||
txi = self.vin[i]
|
||||
txi.value_in = int.from_bytes(data[o:o + 8], 'little')
|
||||
o += 8
|
||||
txi.block_height = int.from_bytes(data[o:o + 4], 'little')
|
||||
o += 4
|
||||
txi.block_index = int.from_bytes(data[o:o + 4], 'little')
|
||||
o += 4
|
||||
script_bytes, nb = decode_compactsize(data, o)
|
||||
o += nb
|
||||
txi.signature_script = data[o:o + script_bytes]
|
||||
o += script_bytes
|
||||
|
||||
def serialize(self, ser_type=TxSerializeType.Full) -> bytes:
|
||||
data = bytes()
|
||||
version = (self.version & 0xffff) | (ser_type << 16)
|
||||
data += version.to_bytes(4, 'little')
|
||||
|
||||
if ser_type == TxSerializeType.Full or ser_type == TxSerializeType.NoWitness:
|
||||
data += encode_compactsize(len(self.vin))
|
||||
for txi in self.vin:
|
||||
data += txi.prevout.hash.to_bytes(32, 'little')
|
||||
data += txi.prevout.n.to_bytes(4, 'little')
|
||||
data += txi.prevout.tree.to_bytes(1, 'little')
|
||||
data += txi.sequence.to_bytes(4, 'little')
|
||||
|
||||
data += encode_compactsize(len(self.vout))
|
||||
for txo in self.vout:
|
||||
data += txo.value.to_bytes(8, 'little')
|
||||
data += txo.version.to_bytes(2, 'little')
|
||||
data += encode_compactsize(len(txo.script_pubkey))
|
||||
data += txo.script_pubkey
|
||||
|
||||
data += self.locktime.to_bytes(4, 'little')
|
||||
data += self.expiry.to_bytes(4, 'little')
|
||||
|
||||
if ser_type == TxSerializeType.Full or ser_type == TxSerializeType.OnlyWitness:
|
||||
data += encode_compactsize(len(self.vin))
|
||||
for txi in self.vin:
|
||||
tc_value_in = txi.value_in & 0xffffffffffffffff # Convert negative values
|
||||
data += tc_value_in.to_bytes(8, 'little')
|
||||
data += txi.block_height.to_bytes(4, 'little')
|
||||
data += txi.block_index.to_bytes(4, 'little')
|
||||
data += encode_compactsize(len(txi.signature_script))
|
||||
data += txi.signature_script
|
||||
|
||||
return data
|
||||
|
||||
def TxHash(self) -> bytes:
|
||||
return blake256(self.serialize(TxSerializeType.NoWitness))[::-1]
|
||||
|
||||
def TxHashWitness(self) -> bytes:
|
||||
raise ValueError('todo')
|
||||
|
||||
def TxHashFull(self) -> bytes:
|
||||
raise ValueError('todo')
|
||||
|
||||
|
||||
def findOutput(tx, script_pk: bytes):
|
||||
for i in range(len(tx.vout)):
|
||||
if tx.vout[i].script_pubkey == script_pk:
|
||||
return i
|
||||
return None
|
||||
47
basicswap/interface/dcr/rpc.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2024 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import json
|
||||
import traceback
|
||||
from basicswap.rpc import Jsonrpc
|
||||
|
||||
|
||||
def callrpc(rpc_port, auth, method, params=[], host='127.0.0.1'):
|
||||
try:
|
||||
url = 'http://{}@{}:{}/'.format(auth, host, rpc_port)
|
||||
x = Jsonrpc(url)
|
||||
x.__handler = None
|
||||
v = x.json_request(method, params)
|
||||
x.close()
|
||||
r = json.loads(v.decode('utf-8'))
|
||||
except Exception as ex:
|
||||
traceback.print_exc()
|
||||
raise ValueError('RPC server error ' + str(ex) + ', method: ' + method)
|
||||
|
||||
if 'error' in r and r['error'] is not None:
|
||||
raise ValueError('RPC error ' + str(r['error']))
|
||||
|
||||
return r['result']
|
||||
|
||||
|
||||
def openrpc(rpc_port, auth, host='127.0.0.1'):
|
||||
try:
|
||||
url = 'http://{}@{}:{}/'.format(auth, host, rpc_port)
|
||||
return Jsonrpc(url)
|
||||
except Exception as ex:
|
||||
traceback.print_exc()
|
||||
raise ValueError('RPC error ' + str(ex))
|
||||
|
||||
|
||||
def make_rpc_func(port, auth, host='127.0.0.1'):
|
||||
port = port
|
||||
auth = auth
|
||||
host = host
|
||||
|
||||
def rpc_func(method, params=None):
|
||||
nonlocal port, auth, host
|
||||
return callrpc(port, auth, method, params, host)
|
||||
return rpc_func
|
||||
50
basicswap/interface/dcr/script.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2024 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
|
||||
OP_0 = 0x00
|
||||
OP_DATA_1 = 0x01
|
||||
OP_1NEGATE = 0x4f
|
||||
OP_1 = 0x51
|
||||
OP_IF = 0x63
|
||||
OP_ELSE = 0x67
|
||||
OP_ENDIF = 0x68
|
||||
OP_DROP = 0x75
|
||||
OP_DUP = 0x76
|
||||
OP_EQUAL = 0x87
|
||||
OP_EQUALVERIFY = 0x88
|
||||
OP_PUSHDATA1 = 0x4c
|
||||
OP_PUSHDATA2 = 0x4d
|
||||
OP_PUSHDATA4 = 0x4e
|
||||
OP_HASH160 = 0xa9
|
||||
OP_CHECKSIG = 0xac
|
||||
OP_CHECKMULTISIG = 0xae
|
||||
OP_CHECKSEQUENCEVERIFY = 0xb2
|
||||
|
||||
|
||||
def push_script_data(data_array: bytearray, data: bytes) -> None:
|
||||
len_data: int = len(data)
|
||||
|
||||
if len_data == 0 or (len_data == 1 and data[0] == 0):
|
||||
data_array += bytes((OP_0,))
|
||||
return
|
||||
if len_data == 1 and data[0] <= 16:
|
||||
data_array += bytes((OP_1 - 1 + data[0],))
|
||||
return
|
||||
if len_data == 1 and data[0] == 0x81:
|
||||
data_array += bytes((OP_1NEGATE,))
|
||||
return
|
||||
|
||||
if len_data < OP_PUSHDATA1:
|
||||
data_array += len_data.to_bytes(1, 'little')
|
||||
elif len_data <= 0xff:
|
||||
data_array += bytes((OP_PUSHDATA1, len_data))
|
||||
elif len_data <= 0xffff:
|
||||
data_array += bytes((OP_PUSHDATA2,)) + len_data.to_bytes(2, 'little')
|
||||
else:
|
||||
data_array += bytes((OP_PUSHDATA4,)) + len_data.to_bytes(4, 'little')
|
||||
|
||||
data_array += data
|
||||
66
basicswap/interface/dcr/util.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2024 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import os
|
||||
import select
|
||||
import subprocess
|
||||
|
||||
|
||||
def createDCRWallet(args, hex_seed, logging, delay_event):
|
||||
logging.info('Creating DCR wallet')
|
||||
|
||||
(pipe_r, pipe_w) = os.pipe() # subprocess.PIPE is buffered, blocks when read
|
||||
|
||||
if os.name == 'nt':
|
||||
str_args = ' '.join(args)
|
||||
p = subprocess.Popen(str_args, shell=True, stdin=subprocess.PIPE, stdout=pipe_w, stderr=pipe_w)
|
||||
else:
|
||||
p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=pipe_w, stderr=pipe_w)
|
||||
|
||||
def readOutput():
|
||||
buf = os.read(pipe_r, 1024).decode('utf-8')
|
||||
response = None
|
||||
if 'Opened wallet' in buf:
|
||||
pass
|
||||
elif 'Use the existing configured private passphrase' in buf:
|
||||
response = b'y\n'
|
||||
elif 'Do you want to add an additional layer of encryption' in buf:
|
||||
response = b'n\n'
|
||||
elif 'Do you have an existing wallet seed' in buf:
|
||||
response = b'y\n'
|
||||
elif 'Enter existing wallet seed' in buf:
|
||||
response = (hex_seed + '\n').encode('utf-8')
|
||||
elif 'Seed input successful' in buf:
|
||||
pass
|
||||
elif 'Upgrading database from version' in buf:
|
||||
pass
|
||||
elif 'Ticket commitments db upgrade done' in buf:
|
||||
pass
|
||||
elif 'The wallet has been created successfully' in buf:
|
||||
pass
|
||||
else:
|
||||
raise ValueError(f'Unexpected output: {buf}')
|
||||
if response is not None:
|
||||
p.stdin.write(response)
|
||||
p.stdin.flush()
|
||||
|
||||
try:
|
||||
while p.poll() is None:
|
||||
if os.name == 'nt':
|
||||
readOutput()
|
||||
delay_event.wait(0.1)
|
||||
continue
|
||||
while len(select.select([pipe_r], [], [], 0)[0]) == 1:
|
||||
readOutput()
|
||||
delay_event.wait(0.1)
|
||||
except Exception as e:
|
||||
logging.error(f'dcrwallet --create failed: {e}')
|
||||
finally:
|
||||
if p.poll() is None:
|
||||
p.terminate()
|
||||
os.close(pipe_r)
|
||||
os.close(pipe_w)
|
||||
p.stdin.close()
|
||||
@@ -1,26 +1,33 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2022 tecnovert
|
||||
# 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 hashlib
|
||||
from .btc import BTCInterface, find_vout_for_address_from_txobj
|
||||
from basicswap.chainparams import Coins
|
||||
import random
|
||||
|
||||
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.contrib.test_framework.script import (
|
||||
from basicswap.chainparams import Coins
|
||||
from basicswap.interface.contrib.firo_test_framework.script import (
|
||||
CScript,
|
||||
OP_0,
|
||||
OP_DUP,
|
||||
OP_EQUAL,
|
||||
OP_HASH160,
|
||||
OP_CHECKSIG,
|
||||
OP_EQUALVERIFY,
|
||||
hash160,
|
||||
)
|
||||
from basicswap.contrib.test_framework.messages import (
|
||||
from basicswap.interface.contrib.firo_test_framework.mininode import (
|
||||
CBlock,
|
||||
FromHex,
|
||||
CTransaction,
|
||||
)
|
||||
|
||||
@@ -30,6 +37,11 @@ class FIROInterface(BTCInterface):
|
||||
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 getExchangeName(self, exchange_name):
|
||||
return 'zcoin'
|
||||
|
||||
@@ -37,10 +49,13 @@ class FIROInterface(BTCInterface):
|
||||
# load with -hdseed= parameter
|
||||
pass
|
||||
|
||||
def checkWallets(self) -> int:
|
||||
return 1
|
||||
|
||||
def getNewAddress(self, use_segwit, label='swap_receive'):
|
||||
return self.rpc_callback('getnewaddress', [label])
|
||||
# addr_plain = self.rpc_callback('getnewaddress', [label])
|
||||
# return self.rpc_callback('addwitnessaddress', [addr_plain])
|
||||
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:]
|
||||
@@ -52,39 +67,38 @@ class FIROInterface(BTCInterface):
|
||||
raise ValueError('TODO')
|
||||
|
||||
def isWatchOnlyAddress(self, address):
|
||||
addr_info = self.rpc_callback('validateaddress', [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_callback('validateaddress', [address])
|
||||
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):
|
||||
def getSCLockScriptAddress(self, lock_script: bytes) -> str:
|
||||
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_callback('importaddress', [lock_tx_dest.hex(), 'bid lock', False, True])
|
||||
addr_info = self.rpc_callback('validateaddress', [address])
|
||||
ro = self.rpc('importaddress', [lock_tx_dest.hex(), 'bid lock', False, True])
|
||||
addr_info = self.rpc('validateaddress', [address])
|
||||
|
||||
return address
|
||||
|
||||
def getLockTxHeightFiro(self, txid, lock_script, bid_amount, rescan_from, find_index=False):
|
||||
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False, vout: int = -1):
|
||||
# Add watchonly address and rescan if required
|
||||
lock_tx_dest = self.getScriptDest(lock_script)
|
||||
dest_address = self.encodeScriptDest(lock_tx_dest)
|
||||
|
||||
if not self.isAddressMine(dest_address, or_watch_only=True):
|
||||
self.rpc_callback('importaddress', [lock_tx_dest.hex(), 'bid lock', False, 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.rpc_callback('rescanblockchain', [rescan_from])
|
||||
self.rescanBlockchainForAddress(rescan_from, dest_address)
|
||||
|
||||
return_txid = True if txid is None else False
|
||||
if txid is None:
|
||||
txns = self.rpc_callback('listunspent', [0, 9999999, [dest_address, ]])
|
||||
txns = self.rpc('listunspent', [0, 9999999, [dest_address, ]])
|
||||
|
||||
for tx in txns:
|
||||
if self.make_int(tx['amount']) == bid_amount:
|
||||
@@ -95,11 +109,11 @@ class FIROInterface(BTCInterface):
|
||||
return None
|
||||
|
||||
try:
|
||||
tx = self.rpc_callback('gettransaction', [txid.hex()])
|
||||
tx = self.rpc('gettransaction', [txid.hex()])
|
||||
|
||||
block_height = 0
|
||||
if 'blockhash' in tx:
|
||||
block_header = self.rpc_callback('getblockheader', [tx['blockhash']])
|
||||
block_header = self.rpc('getblockheader', [tx['blockhash']])
|
||||
block_height = block_header['height']
|
||||
|
||||
rv = {
|
||||
@@ -111,7 +125,7 @@ class FIROInterface(BTCInterface):
|
||||
return None
|
||||
|
||||
if find_index:
|
||||
tx_obj = self.rpc_callback('decoderawtransaction', [tx['hex']])
|
||||
tx_obj = self.rpc('decoderawtransaction', [tx['hex']])
|
||||
rv['index'] = find_vout_for_address_from_txobj(tx_obj, dest_address)
|
||||
|
||||
if return_txid:
|
||||
@@ -130,11 +144,11 @@ class FIROInterface(BTCInterface):
|
||||
return self.fundTx(tx_bytes, feerate)
|
||||
|
||||
def signTxWithWallet(self, tx):
|
||||
rv = self.rpc_callback('signrawtransaction', [tx.hex()])
|
||||
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_callback('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
|
||||
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 = {
|
||||
@@ -143,25 +157,23 @@ class FIROInterface(BTCInterface):
|
||||
}
|
||||
if sub_fee:
|
||||
options['subtractFeeFromOutputs'] = [0,]
|
||||
return self.rpc_callback('fundrawtransaction', [txn, options])['hex']
|
||||
return self.rpc('fundrawtransaction', [txn, options])['hex']
|
||||
|
||||
def createRawSignedTransaction(self, addr_to, amount) -> str:
|
||||
txn_funded = self.createRawFundedTransaction(addr_to, amount)
|
||||
return self.rpc_callback('signrawtransaction', [txn_funded])['hex']
|
||||
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:
|
||||
# P2WSH nested in BIP16_P2SH
|
||||
# P2SH
|
||||
|
||||
script_hash = hashlib.sha256(script).digest()
|
||||
assert len(script_hash) == 32
|
||||
script_hash_hash = hash160(script_hash)
|
||||
assert len(script_hash_hash) == 20
|
||||
script_hash = hash160(script)
|
||||
assert len(script_hash) == 20
|
||||
|
||||
return CScript([OP_HASH160, script_hash_hash, OP_EQUAL])
|
||||
return CScript([OP_HASH160, script_hash, OP_EQUAL])
|
||||
|
||||
def getSeedHash(self, seed: bytes) -> bytes:
|
||||
return hash160(seed)[::-1]
|
||||
@@ -171,35 +183,36 @@ class FIROInterface(BTCInterface):
|
||||
script_hash = script_dest[2:-1]
|
||||
return self.sh_to_address(script_hash)
|
||||
|
||||
def getScriptScriptSig(self, script: bytes) -> bytearray:
|
||||
return CScript([OP_0, hashlib.sha256(script).digest()])
|
||||
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_callback('sendtoaddress', params)
|
||||
return self.rpc('sendtoaddress', params)
|
||||
|
||||
def getWalletSeedID(self):
|
||||
return self.rpc_callback('getwalletinfo')['hdmasterkeyid']
|
||||
return self.rpc('getwalletinfo')['hdmasterkeyid']
|
||||
|
||||
def getSpendableBalance(self) -> int:
|
||||
return self.make_int(self.rpc_callback('getwalletinfo')['balance'])
|
||||
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}.')
|
||||
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_callback('signrawtransaction', [tx.hex(), [], [key_wif, ]])
|
||||
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_callback('gettransaction', [txid_hex])
|
||||
rv = self.rpc('gettransaction', [txid_hex])
|
||||
except Exception as ex:
|
||||
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
|
||||
return None
|
||||
@@ -207,3 +220,148 @@ class FIROInterface(BTCInterface):
|
||||
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()
|
||||
|
||||
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: str):
|
||||
# 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,
|
||||
'previousblockhash': block_header['previousblockhash'],
|
||||
'tx': tx_rv,
|
||||
'confirmations': block_header['confirmations'],
|
||||
'height': block_header['height'],
|
||||
'time': block_header['time'],
|
||||
'version': block_header['version'],
|
||||
'merkleroot': block_header['merkleroot'],
|
||||
}
|
||||
|
||||
return block_rv
|
||||
|
||||
@@ -1,15 +1,150 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2020 tecnovert
|
||||
# Copyright (c) 2020-2023 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.rpc import make_rpc_func
|
||||
from basicswap.chainparams import Coins, chainparams
|
||||
|
||||
|
||||
class LTCInterface(BTCInterface):
|
||||
@staticmethod
|
||||
def coin_type():
|
||||
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 createUTXO(self, value_sats: int):
|
||||
# Create a new address and send value_sats to it
|
||||
|
||||
spendable_balance = self.getSpendableBalance()
|
||||
if spendable_balance < value_sats:
|
||||
raise ValueError('Balance too low')
|
||||
|
||||
address = self.getNewAddress(self._use_segwit, 'create_utxo')
|
||||
return self.withdrawCoin(self.format_amount(value_sats), 'plain', address, False), address
|
||||
|
||||
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']
|
||||
return rv
|
||||
|
||||
def getUnspentsByAddr(self):
|
||||
unspent_addr = dict()
|
||||
unspent = self.rpc_wallet('listunspent')
|
||||
for u in unspent:
|
||||
if u.get('spendable', False) is False:
|
||||
continue
|
||||
if u.get('solvable', False) is False: # Filter out mweb outputs
|
||||
continue
|
||||
if 'address' not in u:
|
||||
continue
|
||||
if 'desc' in u:
|
||||
desc = u['desc']
|
||||
if self.using_segwit:
|
||||
if self.use_p2shp2wsh():
|
||||
if not desc.startswith('sh(wpkh'):
|
||||
continue
|
||||
else:
|
||||
if not desc.startswith('wpkh'):
|
||||
continue
|
||||
else:
|
||||
if not desc.startswith('pkh'):
|
||||
continue
|
||||
unspent_addr[u['address']] = unspent_addr.get(u['address'], 0) + self.make_int(u['amount'], r=1)
|
||||
return unspent_addr
|
||||
|
||||
|
||||
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())
|
||||
|
||||
@@ -5,13 +5,23 @@
|
||||
# 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.interface.btc import (
|
||||
BTCInterface,
|
||||
extractScriptLockRefundScriptValues,
|
||||
findOutput,
|
||||
find_vout_for_address_from_txobj,
|
||||
)
|
||||
from basicswap.rpc import make_rpc_func
|
||||
from basicswap.chainparams import Coins
|
||||
from basicswap.contrib.mnemonic import Mnemonic
|
||||
from basicswap.interface.contrib.nav_test_framework.mininode import (
|
||||
CTxIn,
|
||||
CTxOut,
|
||||
@@ -20,15 +30,15 @@ from basicswap.interface.contrib.nav_test_framework.mininode import (
|
||||
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,
|
||||
b2i, i2b, i2h,
|
||||
ensure,
|
||||
)
|
||||
from basicswap.basicswap_util import (
|
||||
@@ -36,7 +46,6 @@ from basicswap.basicswap_util import (
|
||||
)
|
||||
|
||||
from basicswap.interface.contrib.nav_test_framework.script import (
|
||||
hash160,
|
||||
CScript,
|
||||
OP_0,
|
||||
OP_EQUAL,
|
||||
@@ -44,7 +53,6 @@ from basicswap.interface.contrib.nav_test_framework.script import (
|
||||
SIGHASH_ALL,
|
||||
SegwitVersion1SignatureHash,
|
||||
)
|
||||
from mnemonic import Mnemonic
|
||||
|
||||
|
||||
class NAVInterface(BTCInterface):
|
||||
@@ -60,36 +68,41 @@ class NAVInterface(BTCInterface):
|
||||
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 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
|
||||
# Load with -importmnemonic= parameter
|
||||
pass
|
||||
|
||||
def checkWallets(self) -> int:
|
||||
return 1
|
||||
|
||||
def getWalletSeedID(self):
|
||||
return self.rpc_callback('getwalletinfo')['hdmasterkeyid']
|
||||
return self.rpc('getwalletinfo')['hdmasterkeyid']
|
||||
|
||||
def withdrawCoin(self, value, addr_to: str, subfee: bool):
|
||||
strdzeel = ''
|
||||
params = [addr_to, value, '', '', strdzeel, subfee]
|
||||
return self.rpc_callback('sendtoaddress', params)
|
||||
return self.rpc('sendtoaddress', params)
|
||||
|
||||
def getSpendableBalance(self) -> int:
|
||||
return self.make_int(self.rpc_callback('getwalletinfo')['balance'])
|
||||
return self.make_int(self.rpc('getwalletinfo')['balance'])
|
||||
|
||||
def signTxWithWallet(self, tx: bytes) -> bytes:
|
||||
rv = self.rpc_callback('signrawtransaction', [tx.hex()])
|
||||
rv = self.rpc('signrawtransaction', [tx.hex()])
|
||||
|
||||
return bytes.fromhex(rv['hex'])
|
||||
|
||||
def checkExpectedSeed(self, key_hash: str):
|
||||
try:
|
||||
rv = self.rpc_callback('dumpmnemonic')
|
||||
rv = self.rpc('dumpmnemonic')
|
||||
entropy = Mnemonic('english').to_entropy(rv.split(' '))
|
||||
|
||||
entropy_hash = self.getAddressHashFromKey(entropy)[::-1].hex()
|
||||
@@ -148,33 +161,118 @@ class NAVInterface(BTCInterface):
|
||||
tx.wit.vtxinwit[0].scriptWitness.stack = stack
|
||||
return tx.serialize_with_witness()
|
||||
|
||||
def verifyProofOfFunds(self, address, signature, extra_commit_bytes):
|
||||
self._log.warning('verifyProofOfFunds TODO')
|
||||
# TODO: Port scantxoutset or external lookup or read utxodb directly
|
||||
return 999999 * self.COIN()
|
||||
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()
|
||||
|
||||
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_callback('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
|
||||
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,]
|
||||
|
||||
return self.fundTx(txn, fee_rate, lock_unspents)
|
||||
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_callback('validateaddress', [address])
|
||||
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_callback('signrawtransaction', [txn_funded])['hex']
|
||||
return self.rpc('signrawtransaction', [txn_funded])['hex']
|
||||
|
||||
def getBlockchainInfo(self):
|
||||
rv = self.rpc_callback('getblockchaininfo')
|
||||
rv = self.rpc('getblockchaininfo')
|
||||
synced = round(rv['verificationprogress'], 3)
|
||||
if synced >= 0.997:
|
||||
rv['verificationprogress'] = 1.0
|
||||
@@ -188,7 +286,7 @@ class NAVInterface(BTCInterface):
|
||||
return pubkeyToAddress(self.chainparams_network()['script_address'], script)
|
||||
|
||||
def find_prevout_info(self, txn_hex: str, txn_script: bytes):
|
||||
txjs = self.rpc_callback('decoderawtransaction', [txn_hex])
|
||||
txjs = self.rpc('decoderawtransaction', [txn_hex])
|
||||
n = getVoutByScriptPubKey(txjs, self.getScriptDest(txn_script).hex())
|
||||
|
||||
return {
|
||||
@@ -199,42 +297,16 @@ class NAVInterface(BTCInterface):
|
||||
'amount': txjs['vout'][n]['value']
|
||||
}
|
||||
|
||||
def getProofOfFunds(self, amount_for, extra_commit_bytes):
|
||||
# TODO: Lock unspent and use same output/s to fund bid
|
||||
unspent_addr = self.getUnspentsByAddr()
|
||||
|
||||
sign_for_addr = None
|
||||
for addr, value in unspent_addr.items():
|
||||
if value >= amount_for:
|
||||
sign_for_addr = addr
|
||||
break
|
||||
|
||||
ensure(sign_for_addr is not None, 'Could not find address with enough funds for proof')
|
||||
|
||||
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_callback('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_callback('signmessage', [sign_for_addr, sign_for_addr + '_swap_proof_' + extra_commit_bytes.hex()])
|
||||
|
||||
return (sign_for_addr, signature)
|
||||
|
||||
def getNewAddress(self, use_segwit: bool, label: str = 'swap_receive') -> str:
|
||||
address: str = self.rpc_callback('getnewaddress', [label,])
|
||||
address: str = self.rpc('getnewaddress', [label,])
|
||||
if use_segwit:
|
||||
return self.rpc_callback('addwitnessaddress', [address,])
|
||||
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])
|
||||
prev_txid = b2i(bytes.fromhex(prevout['txid']))
|
||||
|
||||
tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout']),
|
||||
scriptSig=self.getScriptScriptSig(txn_script)))
|
||||
@@ -248,7 +320,7 @@ class NAVInterface(BTCInterface):
|
||||
tx = CTransaction()
|
||||
tx.nVersion = self.txVersion()
|
||||
tx.nLockTime = locktime
|
||||
prev_txid = uint256_from_str(bytes.fromhex(prevout['txid'])[::-1])
|
||||
prev_txid = b2i(bytes.fromhex(prevout['txid']))
|
||||
tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout']),
|
||||
nSequence=sequence,
|
||||
scriptSig=self.getScriptScriptSig(txn_script)))
|
||||
@@ -321,15 +393,15 @@ class NAVInterface(BTCInterface):
|
||||
chain_blocks: int = self.getChainHeight()
|
||||
|
||||
current_height: int = chain_blocks
|
||||
block_hash = self.rpc_callback('getblockhash', [current_height])
|
||||
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_callback('getblockhash', [current_height])
|
||||
block_hash = self.rpc('getblockhash', [current_height])
|
||||
|
||||
block = self.rpc_callback('getblock', [block_hash, False])
|
||||
block = self.rpc('getblock', [block_hash, False])
|
||||
decoded_block = CBlock()
|
||||
decoded_block = FromHex(decoded_block, block)
|
||||
for tx in decoded_block.vtx:
|
||||
@@ -339,24 +411,23 @@ class NAVInterface(BTCInterface):
|
||||
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_callback('invalidateblock', [block_hash])
|
||||
self.rpc_callback('reconsiderblock', [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):
|
||||
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False, vout: int = -1):
|
||||
# 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))
|
||||
# Importing triggers a rescan
|
||||
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_callback('listunspent', [0, 9999999, [dest_address, ]])
|
||||
txns = self.rpc('listunspent', [0, 9999999, [dest_address, ]])
|
||||
|
||||
for tx in txns:
|
||||
if self.make_int(tx['amount']) == bid_amount:
|
||||
@@ -367,11 +438,11 @@ class NAVInterface(BTCInterface):
|
||||
return None
|
||||
|
||||
try:
|
||||
tx = self.rpc_callback('gettransaction', [txid.hex()])
|
||||
tx = self.rpc('gettransaction', [txid.hex()])
|
||||
|
||||
block_height = 0
|
||||
if 'blockhash' in tx:
|
||||
block_header = self.rpc_callback('getblockheader', [tx['blockhash']])
|
||||
block_header = self.rpc('getblockheader', [tx['blockhash']])
|
||||
block_height = block_header['height']
|
||||
|
||||
rv = {
|
||||
@@ -383,7 +454,7 @@ class NAVInterface(BTCInterface):
|
||||
return None
|
||||
|
||||
if find_index:
|
||||
tx_obj = self.rpc_callback('decoderawtransaction', [tx['hex']])
|
||||
tx_obj = self.rpc('decoderawtransaction', [tx['hex']])
|
||||
rv['index'] = find_vout_for_address_from_txobj(tx_obj, dest_address)
|
||||
|
||||
if return_txid:
|
||||
@@ -393,15 +464,15 @@ class NAVInterface(BTCInterface):
|
||||
|
||||
def getBlockWithTxns(self, block_hash):
|
||||
# TODO: Bypass decoderawtransaction and getblockheader
|
||||
block = self.rpc_callback('getblock', [block_hash, False])
|
||||
block_header = self.rpc_callback('getblockheader', [block_hash])
|
||||
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_callback('decoderawtransaction', [tx_hex])
|
||||
tx_dec = self.rpc('decoderawtransaction', [tx_hex])
|
||||
if 'hex' not in tx_dec:
|
||||
tx_dec['hex'] = tx_hex
|
||||
|
||||
@@ -409,16 +480,18 @@ class NAVInterface(BTCInterface):
|
||||
|
||||
block_rv = {
|
||||
'hash': block_hash,
|
||||
'previousblockhash': block_header['previousblockhash'],
|
||||
'tx': tx_rv,
|
||||
'confirmations': block_header['confirmations'],
|
||||
'height': block_header['height'],
|
||||
'time': block_header['time'],
|
||||
'version': block_header['version'],
|
||||
'merkleroot': block_header['merkleroot'],
|
||||
}
|
||||
|
||||
return block_rv
|
||||
|
||||
def getScriptScriptSig(self, script: bytes) -> bytearray:
|
||||
def getScriptScriptSig(self, script: bytes) -> bytes:
|
||||
return self.getP2SHP2WSHScriptSig(script)
|
||||
|
||||
def getScriptDest(self, script):
|
||||
@@ -440,9 +513,9 @@ class NAVInterface(BTCInterface):
|
||||
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:
|
||||
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, lock_tx_vout=None) -> bytes:
|
||||
self._log.info('spendBLockTx %s:\n', chain_b_lock_txid.hex())
|
||||
wtx = self.rpc_callback('gettransaction', [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)
|
||||
@@ -454,7 +527,7 @@ class NAVInterface(BTCInterface):
|
||||
tx = CTransaction()
|
||||
tx.nVersion = self.txVersion()
|
||||
|
||||
chain_b_lock_txid_int = uint256_from_str(chain_b_lock_txid[::-1])
|
||||
chain_b_lock_txid_int = b2i(chain_b_lock_txid)
|
||||
|
||||
script_sig = self.getInputScriptForPubkeyHash(self.getPubkeyHash(Kbs))
|
||||
|
||||
@@ -487,7 +560,7 @@ class NAVInterface(BTCInterface):
|
||||
def findTxnByHash(self, txid_hex: str):
|
||||
# Only works for wallet txns
|
||||
try:
|
||||
rv = self.rpc_callback('gettransaction', [txid_hex])
|
||||
rv = self.rpc('gettransaction', [txid_hex])
|
||||
except Exception as ex:
|
||||
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
|
||||
return None
|
||||
@@ -503,17 +576,17 @@ class NAVInterface(BTCInterface):
|
||||
|
||||
return tx.serialize()
|
||||
|
||||
def fundTx(self, tx, feerate, lock_unspents: bool = True):
|
||||
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_callback('fundrawtransaction', [tx.hex(), options])
|
||||
rv = self.rpc('fundrawtransaction', [tx_hex, options])
|
||||
|
||||
# Sign transaction then strip witness data to fill scriptsig
|
||||
rv = self.rpc_callback('signrawtransaction', [rv['hex']])
|
||||
rv = self.rpc('signrawtransaction', [rv['hex']])
|
||||
|
||||
tx_signed = self.loadTx(bytes.fromhex(rv['hex']))
|
||||
if len(tx_signed.vin) != len(tx_signed.wit.vtxinwit):
|
||||
@@ -524,8 +597,8 @@ class NAVInterface(BTCInterface):
|
||||
|
||||
return tx_signed.serialize_without_witness()
|
||||
|
||||
def fundSCLockTx(self, tx_bytes: bytes, feerate, vkbv=None):
|
||||
tx_funded = self.fundTx(tx_bytes, feerate)
|
||||
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):
|
||||
@@ -606,7 +679,7 @@ class NAVInterface(BTCInterface):
|
||||
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)
|
||||
A, B, lock2_value, C = extractScriptLockRefundScriptValues(script_lock_refund)
|
||||
|
||||
tx_lock_refund.rehash()
|
||||
tx_lock_refund_hash_int = tx_lock_refund.sha256
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
|
||||
from .btc import BTCInterface
|
||||
from basicswap.chainparams import Coins
|
||||
from basicswap.util import (
|
||||
make_int,
|
||||
)
|
||||
|
||||
|
||||
class NMCInterface(BTCInterface):
|
||||
@@ -17,16 +14,16 @@ class NMCInterface(BTCInterface):
|
||||
def coin_type():
|
||||
return Coins.NMC
|
||||
|
||||
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: bool = False, vout: int = -1):
|
||||
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')
|
||||
return_txid = True if txid is None else False
|
||||
for o in ro['unspents']:
|
||||
if txid and o['txid'] != txid.hex():
|
||||
continue
|
||||
# Verify amount
|
||||
if make_int(o['amount']) != int(bid_amount):
|
||||
if self.make_int(o['amount']) != int(bid_amount):
|
||||
self._log.warning('Found output to lock tx address of incorrect value: %s, %s', str(o['amount']), o['txid'])
|
||||
continue
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2020-2023 tecnovert
|
||||
# Copyright (c) 2020-2024 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -14,11 +14,10 @@ from basicswap.contrib.test_framework.messages import (
|
||||
from basicswap.contrib.test_framework.script import (
|
||||
CScript,
|
||||
OP_0,
|
||||
OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG
|
||||
OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG,
|
||||
)
|
||||
from basicswap.util import (
|
||||
ensure,
|
||||
make_int,
|
||||
TemporaryError,
|
||||
)
|
||||
from basicswap.util.script import (
|
||||
@@ -27,10 +26,15 @@ from basicswap.util.script import (
|
||||
getWitnessElementLen,
|
||||
)
|
||||
from basicswap.util.address import (
|
||||
toWIF,
|
||||
encodeStealthAddress)
|
||||
encodeStealthAddress,
|
||||
)
|
||||
from basicswap.interface.btc import (
|
||||
BTCInterface,
|
||||
extractScriptLockScriptValues,
|
||||
extractScriptLockRefundScriptValues,
|
||||
)
|
||||
|
||||
from basicswap.chainparams import Coins, chainparams
|
||||
from .btc import BTCInterface
|
||||
|
||||
|
||||
class BalanceTypes(IntEnum):
|
||||
@@ -74,6 +78,9 @@ class PARTInterface(BTCInterface):
|
||||
super().__init__(coin_settings, network, swap_client)
|
||||
self.setAnonTxRingSize(int(coin_settings.get('anon_tx_ring_size', 12)))
|
||||
|
||||
def use_tx_vsize(self) -> bool:
|
||||
return True
|
||||
|
||||
def setAnonTxRingSize(self, value):
|
||||
ensure(value >= 3 and value < 33, 'Invalid anon_tx_ring_size value')
|
||||
self._anon_tx_ring_size = value
|
||||
@@ -83,29 +90,29 @@ class PARTInterface(BTCInterface):
|
||||
return True
|
||||
|
||||
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') -> str:
|
||||
return self.rpc_callback('getnewstealthaddress', [label])
|
||||
return self.rpc_wallet('getnewstealthaddress', [label])
|
||||
|
||||
def haveSpentIndex(self):
|
||||
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']
|
||||
|
||||
def initialiseWallet(self, key):
|
||||
def initialiseWallet(self, key: bytes) -> None:
|
||||
raise ValueError('TODO')
|
||||
|
||||
def withdrawCoin(self, value, addr_to, subfee):
|
||||
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):
|
||||
params = [type_from, type_to,
|
||||
[{'address': addr_to, 'amount': value, 'subfee': subfee}, ],
|
||||
'', '', self._anon_tx_ring_size, 1, False,
|
||||
{'conf_target': self._conf_target}]
|
||||
return self.rpc_callback('sendtypeto', params)
|
||||
return self.rpc_wallet('sendtypeto', params)
|
||||
|
||||
def getScriptForPubkeyHash(self, pkh: bytes) -> CScript:
|
||||
return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG])
|
||||
@@ -122,7 +129,7 @@ class PARTInterface(BTCInterface):
|
||||
return length
|
||||
|
||||
def getWalletRestoreHeight(self) -> int:
|
||||
start_time = self.rpc_callback('getwalletinfo')['keypoololdest']
|
||||
start_time = self.rpc_wallet('getwalletinfo')['keypoololdest']
|
||||
|
||||
blockchaininfo = self.getBlockchainInfo()
|
||||
best_block = blockchaininfo['bestblockhash']
|
||||
@@ -132,8 +139,8 @@ class PARTInterface(BTCInterface):
|
||||
raise ValueError('{} chain isn\'t synced.'.format(self.coin_name()))
|
||||
|
||||
self._log.debug('Finding block at time: {}'.format(start_time))
|
||||
block_hash = self.rpc_callback('getblockhashafter', [start_time])
|
||||
block_header = self.rpc_callback('getblockheader', [block_hash])
|
||||
block_hash = self.rpc('getblockhashafter', [start_time])
|
||||
block_header = self.rpc('getblockheader', [block_hash])
|
||||
return block_header['height']
|
||||
|
||||
def getHTLCSpendTxVSize(self, redeem: bool = True) -> int:
|
||||
@@ -141,6 +148,17 @@ class PARTInterface(BTCInterface):
|
||||
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):
|
||||
@staticmethod
|
||||
@@ -171,16 +189,16 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
if txo['type'] != 'blind':
|
||||
continue
|
||||
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']
|
||||
|
||||
self.rpc_callback('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], nonce.hex()])
|
||||
self.rpc('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], nonce.hex()])
|
||||
break
|
||||
except Exception as e:
|
||||
self._log.debug('Searching for locked output: {}'.format(str(e)))
|
||||
continue
|
||||
# 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')
|
||||
return output_n, blinded_info
|
||||
|
||||
@@ -195,7 +213,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
inputs = []
|
||||
outputs = [{'type': 'blind', 'amount': self.format_amount(value), 'address': p2wsh_addr, 'nonce': nonce.hex(), 'data': ephemeral_pubkey.hex()}]
|
||||
params = [inputs, outputs]
|
||||
rv = self.rpc_callback('createrawparttransaction', params)
|
||||
rv = self.rpc_wallet('createrawparttransaction', params)
|
||||
|
||||
tx_bytes = bytes.fromhex(rv['hex'])
|
||||
return tx_bytes
|
||||
@@ -207,11 +225,11 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
tx_hex = tx_bytes.hex()
|
||||
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)
|
||||
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()}}
|
||||
|
||||
@@ -219,11 +237,11 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
'lockUnspents': True,
|
||||
'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'])
|
||||
|
||||
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'])
|
||||
# Nonce is derived from vkbv, ephemeral_key isn't used
|
||||
ephemeral_key = self.getNewSecretKey()
|
||||
@@ -244,7 +262,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
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()}]
|
||||
params = [inputs, outputs]
|
||||
rv = self.rpc_callback('createrawparttransaction', params)
|
||||
rv = self.rpc_wallet('createrawparttransaction', params)
|
||||
lock_refund_tx_hex = rv['hex']
|
||||
|
||||
# Set dummy witness data for fee estimation
|
||||
@@ -261,7 +279,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
'feeRate': self.format_amount(tx_fee_rate),
|
||||
'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']
|
||||
|
||||
for vout, txo in rv['output_amounts'].items():
|
||||
@@ -275,7 +293,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
# 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
|
||||
|
||||
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 = self.getScriptLockRefundTxNonce(vkbv)
|
||||
|
||||
@@ -285,7 +303,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
|
||||
tx_lock_refund_id = lock_refund_tx_obj['txid']
|
||||
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']
|
||||
|
||||
# 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
|
||||
@@ -293,7 +311,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
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}]
|
||||
params = [inputs, outputs]
|
||||
rv = self.rpc_callback('createrawparttransaction', params)
|
||||
rv = self.rpc_wallet('createrawparttransaction', params)
|
||||
lock_refund_spend_tx_hex = rv['hex']
|
||||
|
||||
# Set dummy witness data for fee estimation
|
||||
@@ -311,7 +329,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
'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']
|
||||
|
||||
return bytes.fromhex(lock_refund_spend_tx_hex)
|
||||
@@ -321,7 +339,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
Kal, Kaf,
|
||||
feerate,
|
||||
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']
|
||||
self._log.info('Verifying lock tx: {}.'.format(lock_txid_hex))
|
||||
|
||||
@@ -334,21 +352,21 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
ensure(lock_output_n is not None, 'Output not found in tx')
|
||||
|
||||
# Check value
|
||||
locked_txo_value = make_int(blinded_info['amount'])
|
||||
locked_txo_value = self.make_int(blinded_info['amount'])
|
||||
ensure(locked_txo_value == swap_value, 'Bad locked value')
|
||||
|
||||
# Check script
|
||||
lock_txo_scriptpk = bytes.fromhex(lock_tx_obj['vout'][lock_output_n]['scriptPubKey']['hex'])
|
||||
script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()])
|
||||
ensure(lock_txo_scriptpk == script_pk, 'Bad output script')
|
||||
A, B = self.extractScriptLockScriptValues(script_out)
|
||||
A, B = extractScriptLockScriptValues(script_out)
|
||||
ensure(A == Kal, 'Bad script leader pubkey')
|
||||
ensure(B == Kaf, 'Bad script follower pubkey')
|
||||
|
||||
# TODO: Check that inputs are unspent, rangeproofs and commitments sum
|
||||
# Verify fee rate
|
||||
vsize = lock_tx_obj['vsize']
|
||||
fee_paid = make_int(lock_tx_obj['vout'][0]['ct_fee'])
|
||||
fee_paid = self.make_int(lock_tx_obj['vout'][0]['ct_fee'])
|
||||
|
||||
fee_rate_paid = fee_paid * 1000 // vsize
|
||||
|
||||
@@ -363,7 +381,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
def verifySCLockRefundTx(self, tx_bytes, lock_tx_bytes, script_out,
|
||||
prevout_id, prevout_n, prevout_seq, prevout_script,
|
||||
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']
|
||||
self._log.info('Verifying lock refund tx: {}.'.format(lock_refund_txid_hex))
|
||||
|
||||
@@ -383,28 +401,28 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
lock_refund_output_n, blinded_info = self.findOutputByNonce(lock_refund_tx_obj, nonce)
|
||||
ensure(lock_refund_output_n is not None, 'Output not found in tx')
|
||||
|
||||
lock_refund_txo_value = make_int(blinded_info['amount'])
|
||||
lock_refund_txo_value = self.make_int(blinded_info['amount'])
|
||||
|
||||
# Check script
|
||||
lock_refund_txo_scriptpk = bytes.fromhex(lock_refund_tx_obj['vout'][lock_refund_output_n]['scriptPubKey']['hex'])
|
||||
script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()])
|
||||
ensure(lock_refund_txo_scriptpk == script_pk, 'Bad output script')
|
||||
A, B, csv_val, C = self.extractScriptLockRefundScriptValues(script_out)
|
||||
A, B, csv_val, C = extractScriptLockRefundScriptValues(script_out)
|
||||
ensure(A == Kal, 'Bad script pubkey')
|
||||
ensure(B == Kaf, 'Bad script pubkey')
|
||||
ensure(csv_val == csv_val_expect, 'Bad script csv value')
|
||||
ensure(C == Kaf, 'Bad script pubkey')
|
||||
|
||||
# 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]
|
||||
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['inputs_valid'] is True, 'Invalid inputs')
|
||||
|
||||
# Check value
|
||||
fee_paid = make_int(lock_refund_tx_obj['vout'][0]['ct_fee'])
|
||||
fee_paid = self.make_int(lock_refund_tx_obj['vout'][0]['ct_fee'])
|
||||
ensure(swap_value - lock_refund_txo_value == fee_paid, 'Bad output value')
|
||||
|
||||
# Check fee rate
|
||||
@@ -422,7 +440,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
lock_refund_tx_id, prevout_script,
|
||||
Kal,
|
||||
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']
|
||||
self._log.info('Verifying lock refund spend tx: {}.'.format(lock_refund_spend_txid_hex))
|
||||
|
||||
@@ -441,10 +459,10 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
# Follower is not concerned with them as they pay to leader
|
||||
|
||||
# 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]
|
||||
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['inputs_valid'] is True, 'Invalid inputs')
|
||||
|
||||
@@ -452,35 +470,35 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(prevout_script)
|
||||
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
|
||||
vsize = self.getTxVSize(self.loadTx(tx_bytes), add_witness_bytes=witness_bytes)
|
||||
fee_paid = make_int(lock_refund_spend_tx_obj['vout'][0]['ct_fee'])
|
||||
fee_paid = self.make_int(lock_refund_spend_tx_obj['vout'][0]['ct_fee'])
|
||||
fee_rate_paid = fee_paid * 1000 // vsize
|
||||
ensure(self.compareFeeRates(fee_rate_paid, feerate), 'Bad fee rate, expected: {}'.format(feerate))
|
||||
|
||||
return True
|
||||
|
||||
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)
|
||||
output_n, _ = self.findOutputByNonce(lock_tx_obj, nonce)
|
||||
ensure(output_n is not None, 'Output not found in tx')
|
||||
return bytes.fromhex(lock_tx_obj['vout'][output_n]['valueCommitment'])
|
||||
|
||||
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)
|
||||
output_n, _ = self.findOutputByNonce(lock_refund_tx_obj, nonce)
|
||||
ensure(output_n is not None, 'Output not found in tx')
|
||||
return bytes.fromhex(lock_refund_tx_obj['vout'][output_n]['valueCommitment'])
|
||||
|
||||
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)
|
||||
output_n, _ = self.findOutputByNonce(lock_refund_tx_obj, nonce)
|
||||
ensure(output_n is not None, 'Output not found in tx')
|
||||
return output_n
|
||||
|
||||
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']
|
||||
|
||||
ensure(lock_tx_obj['version'] == self.txVersion(), 'Bad version')
|
||||
@@ -496,7 +514,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
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()}]
|
||||
params = [inputs, outputs]
|
||||
rv = self.rpc_callback('createrawparttransaction', params)
|
||||
rv = self.rpc_wallet('createrawparttransaction', params)
|
||||
lock_spend_tx_hex = rv['hex']
|
||||
|
||||
# Set dummy witness data for fee estimation
|
||||
@@ -513,10 +531,10 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
'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_obj = self.rpc_callback('decoderawtransaction', [lock_spend_tx_hex])
|
||||
pay_fee = make_int(lock_spend_tx_obj['vout'][0]['ct_fee'])
|
||||
lock_spend_tx_obj = self.rpc('decoderawtransaction', [lock_spend_tx_hex])
|
||||
pay_fee = self.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)
|
||||
@@ -535,7 +553,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
def verifySCLockSpendTx(self, tx_bytes,
|
||||
lock_tx_bytes, lock_tx_script,
|
||||
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']
|
||||
self._log.info('Verifying lock spend tx: {}.'.format(lock_spend_txid_hex))
|
||||
|
||||
@@ -543,7 +561,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
ensure(lock_spend_tx_obj['locktime'] == 0, 'Bad nLockTime')
|
||||
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']
|
||||
|
||||
# Find the output of the lock tx to verify
|
||||
@@ -559,7 +577,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
ensure(len(lock_spend_tx_obj['vout']) == 3, 'tx doesn\'t have three outputs')
|
||||
|
||||
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:
|
||||
output_blinded_info = None
|
||||
@@ -568,7 +586,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
if txo['type'] != 'blind':
|
||||
continue
|
||||
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']
|
||||
break
|
||||
except Exception as e:
|
||||
@@ -577,19 +595,19 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
ensure(output_n is not None, 'Output not found in tx')
|
||||
|
||||
# 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')
|
||||
|
||||
# Check rangeproofs and commitments sum
|
||||
prevout = lock_tx_obj['vout'][spend_n]
|
||||
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['inputs_valid'] is True, 'Invalid inputs')
|
||||
|
||||
# Check amount
|
||||
fee_paid = make_int(lock_spend_tx_obj['vout'][0]['ct_fee'])
|
||||
amount_difference = make_int(input_blinded_info['amount']) - make_int(output_blinded_info['amount'])
|
||||
fee_paid = self.make_int(lock_spend_tx_obj['vout'][0]['ct_fee'])
|
||||
amount_difference = self.make_int(input_blinded_info['amount']) - self.make_int(output_blinded_info['amount'])
|
||||
ensure(fee_paid == amount_difference, 'Invalid output amount')
|
||||
|
||||
# Check fee
|
||||
@@ -607,7 +625,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
def createSCLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv):
|
||||
# lock refund swipe tx
|
||||
# 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)
|
||||
|
||||
# Find the output of the lock refund tx to spend
|
||||
@@ -616,17 +634,17 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
|
||||
tx_lock_refund_id = lock_refund_tx_obj['txid']
|
||||
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']
|
||||
|
||||
A, B, lock2_value, C = self.extractScriptLockRefundScriptValues(script_lock_refund)
|
||||
A, B, lock2_value, C = extractScriptLockRefundScriptValues(script_lock_refund)
|
||||
|
||||
# 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
|
||||
|
||||
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}]
|
||||
params = [inputs, outputs]
|
||||
rv = self.rpc_callback('createrawparttransaction', params)
|
||||
rv = self.rpc_wallet('createrawparttransaction', params)
|
||||
|
||||
lock_refund_swipe_tx_hex = rv['hex']
|
||||
|
||||
@@ -645,15 +663,15 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
'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']
|
||||
|
||||
return bytes.fromhex(lock_refund_swipe_tx_hex)
|
||||
|
||||
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:
|
||||
def publishBLockTx(self, vkbv: bytes, Kbs: bytes, output_amount: int, feerate: int, unlock_time: int = 0) -> bytes:
|
||||
Kbv = self.getPubkey(vkbv)
|
||||
sx_addr = self.formatStealthAddress(Kbv, Kbs)
|
||||
self._log.debug('sx_addr: {}'.format(sx_addr))
|
||||
@@ -664,7 +682,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
'', '', self._anon_tx_ring_size, 1, False,
|
||||
{'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)
|
||||
|
||||
def findTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height: int, bid_sender: bool):
|
||||
@@ -675,27 +693,26 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
if bid_sender:
|
||||
cb_swap_value *= -1
|
||||
else:
|
||||
addr_info = self.rpc_callback('getaddressinfo', [sx_addr])
|
||||
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_callback('importstealthaddress', [wif_scan_key, Kbs.hex()])
|
||||
wif_scan_key = self.encodeKey(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_callback('rescanblockchain', [restore_height])
|
||||
self.rpc_wallet('rescanblockchain', [restore_height])
|
||||
|
||||
params = [{'include_watchonly': True, 'search': sx_addr}]
|
||||
txns = self.rpc_callback('filtertransactions', params)
|
||||
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:
|
||||
if self.make_int(tx['outputs'][0]['amount']) == cb_swap_value:
|
||||
height = 0
|
||||
if tx['confirmations'] > 0:
|
||||
chain_height = self.rpc_callback('getblockcount')
|
||||
chain_height = self.rpc('getblockcount')
|
||||
height = chain_height - (tx['confirmations'] - 1)
|
||||
return {'txid': tx['txid'], 'amount': cb_swap_value, 'height': height}
|
||||
else:
|
||||
@@ -703,24 +720,23 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
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:
|
||||
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, lock_tx_vout=None) -> bytes:
|
||||
Kbv = self.getPubkey(kbv)
|
||||
Kbs = self.getPubkey(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']:
|
||||
wif_prefix = self.chainparams_network()['key_prefix']
|
||||
wif_scan_key = toWIF(wif_prefix, kbv)
|
||||
wif_spend_key = toWIF(wif_prefix, kbs)
|
||||
self.rpc_callback('importstealthaddress', [wif_scan_key, wif_spend_key])
|
||||
wif_scan_key = self.encodeKey(kbv)
|
||||
wif_spend_key = self.encodeKey(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_callback('rescanblockchain', [restore_height])
|
||||
self.rpc_wallet('rescanblockchain', [restore_height])
|
||||
|
||||
# TODO: Remove workaround
|
||||
# utxos = self.rpc_callback('listunspentblind', [1, 9999999, [sx_addr]])
|
||||
# utxos = self.rpc_wallet('listunspentblind', [1, 9999999, [sx_addr]])
|
||||
utxos = []
|
||||
all_utxos = self.rpc_callback('listunspentblind', [1, 9999999])
|
||||
all_utxos = self.rpc_wallet('listunspentblind', [1, 9999999])
|
||||
for utxo in all_utxos:
|
||||
if utxo.get('stealth_address', '_') == sx_addr:
|
||||
utxos.append(utxo)
|
||||
@@ -730,7 +746,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
raise ValueError('Too many spendable outputs')
|
||||
|
||||
utxo = utxos[0]
|
||||
utxo_sats = make_int(utxo['amount'])
|
||||
utxo_sats = self.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))
|
||||
@@ -741,14 +757,14 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
[{'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_callback('sendtypeto', params)
|
||||
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_callback('getrawtransaction', [txid_hex, True])
|
||||
rv = self.rpc('getrawtransaction', [txid_hex, True])
|
||||
except Exception as ex:
|
||||
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
|
||||
return None
|
||||
@@ -759,7 +775,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
return None
|
||||
|
||||
def createRawFundedTransaction(self, addr_to: str, amount: int, sub_fee: bool = False, lock_unspents: bool = True) -> str:
|
||||
txn = self.rpc_callback('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
|
||||
txn = self.rpc_wallet('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
|
||||
|
||||
options = {
|
||||
'lockUnspents': lock_unspents,
|
||||
@@ -767,7 +783,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
}
|
||||
if sub_fee:
|
||||
options['subtractFeeFromOutputs'] = [0,]
|
||||
return self.rpc_callback('fundrawtransactionfrom', ['blind', txn, options])['hex']
|
||||
return self.rpc_wallet('fundrawtransactionfrom', ['blind', txn, options])['hex']
|
||||
|
||||
|
||||
class PARTInterfaceAnon(PARTInterface):
|
||||
@@ -791,7 +807,7 @@ class PARTInterfaceAnon(PARTInterface):
|
||||
def coin_name(self) -> str:
|
||||
return super().coin_name() + ' Anon'
|
||||
|
||||
def publishBLockTx(self, kbv: bytes, Kbs: bytes, output_amount: int, feerate: int, delay_for: int = 10, unlock_time: int = 0) -> bytes:
|
||||
def publishBLockTx(self, kbv: bytes, Kbs: bytes, output_amount: int, feerate: int, unlock_time: int = 0) -> bytes:
|
||||
Kbv = self.getPubkey(kbv)
|
||||
sx_addr = self.formatStealthAddress(Kbv, Kbs)
|
||||
|
||||
@@ -801,7 +817,7 @@ class PARTInterfaceAnon(PARTInterface):
|
||||
'', '', self._anon_tx_ring_size, 1, False,
|
||||
{'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)
|
||||
|
||||
def findTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender):
|
||||
@@ -813,27 +829,26 @@ class PARTInterfaceAnon(PARTInterface):
|
||||
if bid_sender:
|
||||
cb_swap_value *= -1
|
||||
else:
|
||||
addr_info = self.rpc_callback('getaddressinfo', [sx_addr])
|
||||
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_callback('importstealthaddress', [wif_scan_key, Kbs.hex()])
|
||||
wif_scan_key = self.encodeKey(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_callback('rescanblockchain', [restore_height])
|
||||
self.rpc_wallet('rescanblockchain', [restore_height])
|
||||
|
||||
params = [{'include_watchonly': True, 'search': sx_addr}]
|
||||
txns = self.rpc_callback('filtertransactions', params)
|
||||
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'] == 'anon', 'Output is not anon')
|
||||
|
||||
if make_int(tx['outputs'][0]['amount']) == cb_swap_value:
|
||||
if self.make_int(tx['outputs'][0]['amount']) == cb_swap_value:
|
||||
height = 0
|
||||
if tx['confirmations'] > 0:
|
||||
chain_height = self.rpc_callback('getblockcount')
|
||||
chain_height = self.rpc('getblockcount')
|
||||
height = chain_height - (tx['confirmations'] - 1)
|
||||
return {'txid': tx['txid'], 'amount': cb_swap_value, 'height': height}
|
||||
else:
|
||||
@@ -841,21 +856,20 @@ class PARTInterfaceAnon(PARTInterface):
|
||||
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:
|
||||
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, lock_tx_vout=None) -> bytes:
|
||||
Kbv = self.getPubkey(kbv)
|
||||
Kbs = self.getPubkey(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']:
|
||||
wif_prefix = self.chainparams_network()['key_prefix']
|
||||
wif_scan_key = toWIF(wif_prefix, kbv)
|
||||
wif_spend_key = toWIF(wif_prefix, kbs)
|
||||
self.rpc_callback('importstealthaddress', [wif_scan_key, wif_spend_key])
|
||||
wif_scan_key = self.encodeKey(kbv)
|
||||
wif_spend_key = self.encodeKey(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_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:
|
||||
raise TemporaryError('No spendable outputs')
|
||||
@@ -863,7 +877,7 @@ class PARTInterfaceAnon(PARTInterface):
|
||||
raise ValueError('Too many spendable outputs')
|
||||
|
||||
utxo = autxos[0]
|
||||
utxo_sats = make_int(utxo['amount'])
|
||||
utxo_sats = self.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))
|
||||
@@ -874,14 +888,14 @@ class PARTInterfaceAnon(PARTInterface):
|
||||
[{'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_callback('sendtypeto', params)
|
||||
rv = self.rpc_wallet('sendtypeto', params)
|
||||
return bytes.fromhex(rv['txid'])
|
||||
|
||||
def findTxnByHash(self, txid_hex: str):
|
||||
# txindex is enabled for Particl
|
||||
|
||||
try:
|
||||
rv = self.rpc_callback('getrawtransaction', [txid_hex, True])
|
||||
rv = self.rpc('getrawtransaction', [txid_hex, True])
|
||||
except Exception as ex:
|
||||
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
|
||||
return None
|
||||
@@ -892,4 +906,4 @@ class PARTInterfaceAnon(PARTInterface):
|
||||
return None
|
||||
|
||||
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'])
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
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 (
|
||||
@@ -29,12 +30,20 @@ class PIVXInterface(BTCInterface):
|
||||
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_callback('signrawtransaction', [tx.hex()])
|
||||
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_callback('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
|
||||
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 = {
|
||||
@@ -43,32 +52,34 @@ class PIVXInterface(BTCInterface):
|
||||
}
|
||||
if sub_fee:
|
||||
options['subtractFeeFromOutputs'] = [0,]
|
||||
return self.rpc_callback('fundrawtransaction', [txn, options])['hex']
|
||||
return self.rpc('fundrawtransaction', [txn, options])['hex']
|
||||
|
||||
def createRawSignedTransaction(self, addr_to, amount) -> str:
|
||||
txn_funded = self.createRawFundedTransaction(addr_to, amount)
|
||||
return self.rpc_callback('signrawtransaction', [txn_funded])['hex']
|
||||
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_callback('getblock', [block_hash, False])
|
||||
block_header = self.rpc_callback('getblockheader', [block_hash])
|
||||
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_callback('decoderawtransaction', [ToHex(tx)])
|
||||
tx_dec = self.rpc('decoderawtransaction', [ToHex(tx)])
|
||||
tx_rv.append(tx_dec)
|
||||
|
||||
block_rv = {
|
||||
'hash': block_hash,
|
||||
'previousblockhash': block_header['previousblockhash'],
|
||||
'tx': tx_rv,
|
||||
'confirmations': block_header['confirmations'],
|
||||
'height': block_header['height'],
|
||||
'time': block_header['time'],
|
||||
'version': block_header['version'],
|
||||
'merkleroot': block_header['merkleroot'],
|
||||
}
|
||||
@@ -77,10 +88,10 @@ class PIVXInterface(BTCInterface):
|
||||
|
||||
def withdrawCoin(self, value, addr_to, subfee):
|
||||
params = [addr_to, value, '', '', subfee]
|
||||
return self.rpc_callback('sendtoaddress', params)
|
||||
return self.rpc('sendtoaddress', params)
|
||||
|
||||
def getSpendableBalance(self) -> int:
|
||||
return self.make_int(self.rpc_callback('getwalletinfo')['balance'])
|
||||
return self.make_int(self.rpc('getwalletinfo')['balance'])
|
||||
|
||||
def loadTx(self, tx_bytes):
|
||||
# Load tx from bytes to internal representation
|
||||
@@ -96,18 +107,18 @@ class PIVXInterface(BTCInterface):
|
||||
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}.')
|
||||
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_callback('signrawtransaction', [tx.hex(), [], [key_wif, ]])
|
||||
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_callback('gettransaction', [txid_hex])
|
||||
rv = self.rpc('gettransaction', [txid_hex])
|
||||
except Exception as ex:
|
||||
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
|
||||
return None
|
||||
|
||||
31
basicswap/interface/wow.py
Normal file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
from basicswap.chainparams import WOW_COIN, Coins
|
||||
from .xmr import XMRInterface
|
||||
|
||||
|
||||
class WOWInterface(XMRInterface):
|
||||
|
||||
@staticmethod
|
||||
def coin_type():
|
||||
return Coins.WOW
|
||||
|
||||
@staticmethod
|
||||
def ticker_str() -> int:
|
||||
return Coins.WOW.name
|
||||
|
||||
@staticmethod
|
||||
def COIN():
|
||||
return WOW_COIN
|
||||
|
||||
@staticmethod
|
||||
def exp() -> int:
|
||||
return 11
|
||||
|
||||
@staticmethod
|
||||
def depth_spendable() -> int:
|
||||
return 3
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2020-2023 tecnovert
|
||||
# Copyright (c) 2020-2024 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -24,20 +24,21 @@ from coincurve.dleag import (
|
||||
verify_ed25519_point,
|
||||
)
|
||||
|
||||
from basicswap.interface import (
|
||||
Curves)
|
||||
from basicswap.interface.base import (
|
||||
Curves,
|
||||
)
|
||||
from basicswap.util import (
|
||||
i2b,
|
||||
i2b, b2i, b2h,
|
||||
dumpj,
|
||||
ensure,
|
||||
make_int,
|
||||
TemporaryError)
|
||||
from basicswap.util.network import (
|
||||
is_private_ip_address)
|
||||
from basicswap.rpc_xmr import (
|
||||
make_xmr_rpc_func,
|
||||
make_xmr_rpc2_func)
|
||||
from basicswap.util import (
|
||||
b2i, b2h)
|
||||
from basicswap.chainparams import XMR_COIN, CoinInterface, Coins
|
||||
from basicswap.chainparams import XMR_COIN, Coins
|
||||
from basicswap.interface.base import CoinInterface
|
||||
|
||||
|
||||
class XMRInterface(CoinInterface):
|
||||
@@ -49,6 +50,10 @@ class XMRInterface(CoinInterface):
|
||||
def coin_type():
|
||||
return Coins.XMR
|
||||
|
||||
@staticmethod
|
||||
def ticker_str() -> int:
|
||||
return Coins.XMR.name
|
||||
|
||||
@staticmethod
|
||||
def COIN():
|
||||
return XMR_COIN
|
||||
@@ -76,16 +81,12 @@ class XMRInterface(CoinInterface):
|
||||
@staticmethod
|
||||
def xmr_swap_b_lock_spend_tx_vsize() -> int:
|
||||
# TODO: Estimate with ringsize
|
||||
return 1507
|
||||
return 1604
|
||||
|
||||
def __init__(self, coin_settings, network, swap_client=None):
|
||||
super().__init__(network)
|
||||
daemon_login = None
|
||||
if coin_settings.get('rpcuser', '') != '':
|
||||
daemon_login = (coin_settings.get('rpcuser', ''), coin_settings.get('rpcpassword', ''))
|
||||
self.rpc_cb = make_xmr_rpc_func(coin_settings['rpcport'], daemon_login, host=coin_settings.get('rpchost', '127.0.0.1'))
|
||||
self.rpc_cb2 = make_xmr_rpc2_func(coin_settings['rpcport'], daemon_login, host=coin_settings.get('rpchost', '127.0.0.1')) # non-json endpoint
|
||||
self.rpc_wallet_cb = make_xmr_rpc_func(coin_settings['walletrpcport'], coin_settings['walletrpcauth'], host=coin_settings.get('walletrpchost', '127.0.0.1'))
|
||||
|
||||
self._addr_prefix = self.chainparams_network()['address_prefix']
|
||||
|
||||
self.blocks_confirmed = coin_settings['blocks_confirmed']
|
||||
self._restore_height = coin_settings.get('restore_height', 0)
|
||||
@@ -95,6 +96,43 @@ class XMRInterface(CoinInterface):
|
||||
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 setFeePriority(self, new_priority):
|
||||
ensure(new_priority >= 0 and new_priority < 4, 'Invalid fee_priority value')
|
||||
self._fee_priority = new_priority
|
||||
@@ -105,7 +143,7 @@ class XMRInterface(CoinInterface):
|
||||
def createWallet(self, params):
|
||||
if self._wallet_password is not None:
|
||||
params['password'] = self._wallet_password
|
||||
rv = self.rpc_wallet_cb('generate_from_keys', params)
|
||||
rv = self.rpc_wallet('generate_from_keys', params)
|
||||
self._log.info('generate_from_keys %s', dumpj(rv))
|
||||
|
||||
def openWallet(self, filename):
|
||||
@@ -115,12 +153,12 @@ class XMRInterface(CoinInterface):
|
||||
|
||||
try:
|
||||
# Can't reopen the same wallet in windows, !is_keys_file_locked()
|
||||
self.rpc_wallet_cb('close_wallet')
|
||||
self.rpc_wallet('close_wallet')
|
||||
except Exception:
|
||||
pass
|
||||
self.rpc_wallet_cb('open_wallet', params)
|
||||
self.rpc_wallet('open_wallet', params)
|
||||
|
||||
def initialiseWallet(self, key_view, key_spend, restore_height=None):
|
||||
def initialiseWallet(self, key_view: bytes, key_spend: bytes, restore_height=None) -> None:
|
||||
with self._mx_wallet:
|
||||
try:
|
||||
self.openWallet(self._wallet_filename)
|
||||
@@ -131,7 +169,7 @@ class XMRInterface(CoinInterface):
|
||||
|
||||
Kbv = self.getPubkey(key_view)
|
||||
Kbs = self.getPubkey(key_spend)
|
||||
address_b58 = xmr_util.encode_address(Kbv, Kbs)
|
||||
address_b58 = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
|
||||
|
||||
params = {
|
||||
'filename': self._wallet_filename,
|
||||
@@ -147,14 +185,14 @@ class XMRInterface(CoinInterface):
|
||||
with self._mx_wallet:
|
||||
self.openWallet(self._wallet_filename)
|
||||
|
||||
def testDaemonRPC(self, with_wallet=True):
|
||||
self.rpc_wallet_cb('get_languages')
|
||||
def testDaemonRPC(self, with_wallet=True) -> None:
|
||||
self.rpc_wallet('get_languages')
|
||||
|
||||
def getDaemonVersion(self):
|
||||
return self.rpc_wallet_cb('get_version')['version']
|
||||
return self.rpc_wallet('get_version')['version']
|
||||
|
||||
def getBlockchainInfo(self):
|
||||
get_height = self.rpc_cb2('get_height', timeout=30)
|
||||
get_height = self.rpc2('get_height', timeout=self._rpctimeout)
|
||||
rv = {
|
||||
'blocks': get_height['height'],
|
||||
'verificationprogress': 0.0,
|
||||
@@ -165,7 +203,7 @@ class XMRInterface(CoinInterface):
|
||||
# get_block_count returns "Internal error" if bootstrap-daemon is active
|
||||
if get_height['untrusted'] is 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:
|
||||
rv['blocks'] = get_info['height_without_bootstrap']
|
||||
|
||||
@@ -173,16 +211,16 @@ class XMRInterface(CoinInterface):
|
||||
if rv['known_block_count'] > rv['blocks']:
|
||||
rv['verificationprogress'] = rv['blocks'] / rv['known_block_count']
|
||||
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']
|
||||
except Exception as e:
|
||||
self._log.warning('XMR get_block_count failed with: %s', str(e))
|
||||
self._log.warning(f'{self.ticker_str()} get_block_count failed with: {e}')
|
||||
rv['verificationprogress'] = 0.0
|
||||
|
||||
return rv
|
||||
|
||||
def getChainHeight(self):
|
||||
return self.rpc_cb2('get_height', timeout=30)['height']
|
||||
return self.rpc2('get_height', timeout=self._rpctimeout)['height']
|
||||
|
||||
def getWalletInfo(self):
|
||||
with self._mx_wallet:
|
||||
@@ -195,32 +233,37 @@ class XMRInterface(CoinInterface):
|
||||
raise e
|
||||
|
||||
rv = {}
|
||||
self.rpc_wallet_cb('refresh')
|
||||
balance_info = self.rpc_wallet_cb('get_balance')
|
||||
self.rpc_wallet('refresh')
|
||||
balance_info = self.rpc_wallet('get_balance')
|
||||
|
||||
rv['balance'] = self.format_amount(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
|
||||
|
||||
def walletRestoreHeight(self):
|
||||
return self._restore_height
|
||||
|
||||
def getMainWalletAddress(self) -> str:
|
||||
with self._mx_wallet:
|
||||
self.openWallet(self._wallet_filename)
|
||||
return self.rpc_wallet_cb('get_address')['address']
|
||||
return self.rpc_wallet('get_address')['address']
|
||||
|
||||
def getNewAddress(self, placeholder) -> str:
|
||||
with self._mx_wallet:
|
||||
self.openWallet(self._wallet_filename)
|
||||
new_address = self.rpc_wallet_cb('create_address', {'account_index': 0})['address']
|
||||
self.rpc_wallet_cb('store')
|
||||
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: int = 2):
|
||||
self._log.warning('TODO - estimate fee rate?')
|
||||
return 0.0, 'unused'
|
||||
# fees - array of unsigned int; Represents the base fees at different priorities [slow, normal, fast, fastest].
|
||||
fee_est = self.rpc('get_fee_estimate')
|
||||
if conf_target <= 1:
|
||||
conf_target = 1 # normal
|
||||
else:
|
||||
conf_target = 0 # slow
|
||||
fee_per_k_bytes = fee_est['fees'][conf_target] * 1000
|
||||
|
||||
return float(self.format_amount(fee_per_k_bytes)), 'get_fee_estimate'
|
||||
|
||||
def getNewSecretKey(self) -> bytes:
|
||||
# Note: Returned bytes are in big endian order
|
||||
@@ -247,7 +290,7 @@ class XMRInterface(CoinInterface):
|
||||
def getAddressFromKeys(self, key_view: bytes, key_spend: bytes) -> str:
|
||||
pk_view = self.getPubkey(key_view)
|
||||
pk_spend = self.getPubkey(key_spend)
|
||||
return xmr_util.encode_address(pk_view, pk_spend)
|
||||
return xmr_util.encode_address(pk_view, pk_spend, self._addr_prefix)
|
||||
|
||||
def verifyKey(self, k: int) -> bool:
|
||||
i = b2i(k)
|
||||
@@ -275,41 +318,29 @@ class XMRInterface(CoinInterface):
|
||||
return ed25519_add(Ka, Kb)
|
||||
|
||||
def encodeSharedAddress(self, Kbv: bytes, Kbs: bytes) -> str:
|
||||
return xmr_util.encode_address(Kbv, Kbs)
|
||||
return xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
|
||||
|
||||
def publishBLockTx(self, kbv: bytes, Kbs: bytes, output_amount: int, feerate: int, delay_for: int = 10, unlock_time: int = 0) -> bytes:
|
||||
def publishBLockTx(self, kbv: bytes, Kbs: bytes, output_amount: int, feerate: int, unlock_time: int = 0) -> bytes:
|
||||
with self._mx_wallet:
|
||||
self.openWallet(self._wallet_filename)
|
||||
self.rpc_wallet_cb('refresh')
|
||||
self.rpc_wallet('refresh')
|
||||
|
||||
Kbv = self.getPubkey(kbv)
|
||||
shared_addr = xmr_util.encode_address(Kbv, Kbs)
|
||||
shared_addr = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
|
||||
|
||||
params = {'destinations': [{'amount': output_amount, 'address': shared_addr}], 'unlock_time': unlock_time}
|
||||
if self._fee_priority > 0:
|
||||
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)
|
||||
tx_hash = bytes.fromhex(rv['tx_hash'])
|
||||
|
||||
if self._sc.debug:
|
||||
i = 0
|
||||
while not self._sc.delay_event.is_set():
|
||||
gt_params = {'out': True, 'pending': True, 'failed': True, 'pool': True, }
|
||||
rv = self.rpc_wallet_cb('get_transfers', gt_params)
|
||||
self._log.debug('get_transfers {}'.format(dumpj(rv)))
|
||||
if 'pending' not in rv:
|
||||
break
|
||||
if i >= delay_for:
|
||||
break
|
||||
self._sc.delay_event.wait(1.0)
|
||||
|
||||
return tx_hash
|
||||
|
||||
def findTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender):
|
||||
with self._mx_wallet:
|
||||
Kbv = self.getPubkey(kbv)
|
||||
address_b58 = xmr_util.encode_address(Kbv, Kbs)
|
||||
address_b58 = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
|
||||
|
||||
kbv_le = kbv[::-1]
|
||||
params = {
|
||||
@@ -325,26 +356,26 @@ class XMRInterface(CoinInterface):
|
||||
self.createWallet(params)
|
||||
self.openWallet(address_b58)
|
||||
|
||||
self.rpc_wallet_cb('refresh', timeout=600)
|
||||
self.rpc_wallet('refresh', timeout=self._walletrpctimeoutlong)
|
||||
|
||||
'''
|
||||
# Debug
|
||||
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)
|
||||
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
|
||||
# and (current_height is None or current_height - transfer['block_height'] > cb_block_confirmed):
|
||||
'''
|
||||
params = {'transfer_type': 'available'}
|
||||
transfers = self.rpc_wallet_cb('incoming_transfers', params)
|
||||
transfers = self.rpc_wallet('incoming_transfers', params)
|
||||
rv = None
|
||||
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_cb('get_transfer_by_txid', {'txid': transfer['tx_hash']})
|
||||
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))
|
||||
@@ -360,17 +391,17 @@ class XMRInterface(CoinInterface):
|
||||
def findTxnByHash(self, txid):
|
||||
with self._mx_wallet:
|
||||
self.openWallet(self._wallet_filename)
|
||||
self.rpc_wallet_cb('refresh', timeout=600)
|
||||
self.rpc_wallet('refresh', timeout=self._walletrpctimeoutlong)
|
||||
|
||||
try:
|
||||
current_height = self.rpc_cb2('get_height', timeout=30)['height']
|
||||
self._log.info('findTxnByHash XMR current_height %d\nhash: %s', current_height, txid)
|
||||
current_height = self.rpc2('get_height', timeout=self._rpctimeout)['height']
|
||||
self._log.info(f'findTxnByHash {self.ticker_str()} current_height {current_height}\nhash: {txid}')
|
||||
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
|
||||
|
||||
params = {'transfer_type': 'available'}
|
||||
rv = self.rpc_wallet_cb('incoming_transfers', params)
|
||||
rv = self.rpc_wallet('incoming_transfers', params)
|
||||
if 'transfers' in rv:
|
||||
for transfer in rv['transfers']:
|
||||
if transfer['tx_hash'] == txid \
|
||||
@@ -379,7 +410,7 @@ class XMRInterface(CoinInterface):
|
||||
|
||||
return None
|
||||
|
||||
def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee_rate: int, restore_height: int, spend_actual_balance: bool = False) -> bytes:
|
||||
def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee_rate: int, restore_height: int, spend_actual_balance: bool = False, lock_tx_vout=None) -> bytes:
|
||||
'''
|
||||
Notes:
|
||||
"Error: No unlocked balance in the specified subaddress(es)" can mean not enough funds after tx fee.
|
||||
@@ -387,7 +418,7 @@ class XMRInterface(CoinInterface):
|
||||
with self._mx_wallet:
|
||||
Kbv = self.getPubkey(kbv)
|
||||
Kbs = self.getPubkey(kbs)
|
||||
address_b58 = xmr_util.encode_address(Kbv, Kbs)
|
||||
address_b58 = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
|
||||
|
||||
wallet_filename = address_b58 + '_spend'
|
||||
|
||||
@@ -405,11 +436,11 @@ class XMRInterface(CoinInterface):
|
||||
self.createWallet(params)
|
||||
self.openWallet(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:
|
||||
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:
|
||||
txns = txns['out']
|
||||
if len(txns) > 0:
|
||||
@@ -434,41 +465,47 @@ class XMRInterface(CoinInterface):
|
||||
if self._fee_priority > 0:
|
||||
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)))
|
||||
|
||||
return bytes.fromhex(rv['tx_hash_list'][0])
|
||||
|
||||
def withdrawCoin(self, value: int, addr_to: str, subfee: bool) -> str:
|
||||
def withdrawCoin(self, value, addr_to: str, sweepall: bool, estimate_fee: bool = False) -> str:
|
||||
with self._mx_wallet:
|
||||
value_sats = make_int(value, self.exp())
|
||||
|
||||
self.openWallet(self._wallet_filename)
|
||||
self.rpc_wallet_cb('refresh')
|
||||
self.rpc_wallet('refresh')
|
||||
|
||||
if subfee:
|
||||
balance = self.rpc_wallet_cb('get_balance')
|
||||
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.')
|
||||
params = {'address': addr_to}
|
||||
if self._fee_priority > 0:
|
||||
params['priority'] = self._fee_priority
|
||||
rv = self.rpc_wallet_cb('sweep_all', params)
|
||||
return rv['tx_hash_list'][0]
|
||||
raise ValueError('Withdraw value must be close to total to use subfee/sweep_all.')
|
||||
if sweepall:
|
||||
balance = self.rpc_wallet('get_balance')
|
||||
if balance['balance'] != balance['unlocked_balance']:
|
||||
raise ValueError('Balance must be fully confirmed to use sweep all.')
|
||||
self._log.info('{} {} sweep_all.'.format(self.ticker_str(), 'estimate fee' if estimate_fee else 'withdraw'))
|
||||
self._log.debug('{} balance: {}'.format(self.ticker_str(), balance['balance']))
|
||||
params = {'address': addr_to, 'do_not_relay': estimate_fee, 'subaddr_indices_all': True}
|
||||
if self._fee_priority > 0:
|
||||
params['priority'] = self._fee_priority
|
||||
rv = self.rpc_wallet('sweep_all', params)
|
||||
if estimate_fee:
|
||||
return {'num_txns': len(rv['fee_list']), 'sum_amount': sum(rv['amount_list']), 'sum_fee': sum(rv['fee_list']), 'sum_weight': sum(rv['weight_list'])}
|
||||
return rv['tx_hash_list'][0]
|
||||
|
||||
params = {'destinations': [{'amount': value_sats, 'address': addr_to}]}
|
||||
value_sats: int = self.make_int(value)
|
||||
params = {'destinations': [{'amount': value_sats, 'address': addr_to}], 'do_not_relay': estimate_fee}
|
||||
if self._fee_priority > 0:
|
||||
params['priority'] = self._fee_priority
|
||||
rv = self.rpc_wallet_cb('transfer', params)
|
||||
rv = self.rpc_wallet('transfer', params)
|
||||
if estimate_fee:
|
||||
return {'num_txns': 1, 'sum_amount': rv['amount'], 'sum_fee': rv['fee'], 'sum_weight': rv['weight']}
|
||||
return rv['tx_hash']
|
||||
|
||||
def estimateFee(self, value: int, addr_to: str, sweepall: bool) -> str:
|
||||
return self.withdrawCoin(value, addr_to, sweepall, estimate_fee=True)
|
||||
|
||||
def showLockTransfers(self, kbv, Kbs, restore_height):
|
||||
with self._mx_wallet:
|
||||
try:
|
||||
Kbv = self.getPubkey(kbv)
|
||||
address_b58 = xmr_util.encode_address(Kbv, Kbs)
|
||||
address_b58 = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
|
||||
wallet_file = address_b58 + '_spend'
|
||||
try:
|
||||
self.openWallet(wallet_file)
|
||||
@@ -488,9 +525,9 @@ class XMRInterface(CoinInterface):
|
||||
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
|
||||
return rv
|
||||
except Exception as e:
|
||||
@@ -500,8 +537,8 @@ class XMRInterface(CoinInterface):
|
||||
with self._mx_wallet:
|
||||
self.openWallet(self._wallet_filename)
|
||||
|
||||
self.rpc_wallet_cb('refresh')
|
||||
balance_info = self.rpc_wallet_cb('get_balance')
|
||||
self.rpc_wallet('refresh')
|
||||
balance_info = self.rpc_wallet('get_balance')
|
||||
return balance_info['unlocked_balance']
|
||||
|
||||
def changeWalletPassword(self, old_password, new_password):
|
||||
@@ -511,7 +548,7 @@ class XMRInterface(CoinInterface):
|
||||
self._wallet_password = old_password
|
||||
try:
|
||||
self.openWallet(self._wallet_filename)
|
||||
self.rpc_wallet_cb('change_wallet_password', {'old_password': old_password, 'new_password': new_password})
|
||||
self.rpc_wallet('change_wallet_password', {'old_password': old_password, 'new_password': new_password})
|
||||
except Exception as e:
|
||||
self._wallet_password = orig_password
|
||||
raise e
|
||||
@@ -536,4 +573,4 @@ class XMRInterface(CoinInterface):
|
||||
raise ValueError('Balance too low')
|
||||
|
||||
def getTransaction(self, txid: bytes):
|
||||
return self.rpc_cb2('get_transactions', {'txs_hashes': [txid.hex(), ]})
|
||||
return self.rpc2('get_transactions', {'txs_hashes': [txid.hex(), ]})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2020-2023 tecnovert
|
||||
# Copyright (c) 2020-2024 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -52,17 +52,30 @@ def getFormData(post_string: str, is_json: bool):
|
||||
|
||||
def withdraw_coin(swap_client, coin_type, post_string, is_json):
|
||||
post_data = getFormData(post_string, is_json)
|
||||
|
||||
value = get_data_entry(post_data, 'value')
|
||||
address = get_data_entry(post_data, 'address')
|
||||
subfee = get_data_entry(post_data, 'subfee')
|
||||
if not isinstance(subfee, bool):
|
||||
subfee = toBool(subfee)
|
||||
|
||||
if coin_type in (Coins.XMR, Coins.WOW):
|
||||
value = None
|
||||
sweepall = get_data_entry(post_data, 'sweepall')
|
||||
if not isinstance(sweepall, bool):
|
||||
sweepall = toBool(sweepall)
|
||||
if not sweepall:
|
||||
value = get_data_entry(post_data, 'value')
|
||||
else:
|
||||
value = get_data_entry(post_data, 'value')
|
||||
subfee = get_data_entry(post_data, 'subfee')
|
||||
if not isinstance(subfee, bool):
|
||||
subfee = toBool(subfee)
|
||||
|
||||
if coin_type == Coins.PART:
|
||||
type_from = get_data_entry_or(post_data, 'type_from', 'plain')
|
||||
type_to = get_data_entry_or(post_data, 'type_to', 'plain')
|
||||
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)
|
||||
elif coin_type in (Coins.XMR, Coins.WOW):
|
||||
txid_hex = swap_client.withdrawCoin(coin_type, value, address, sweepall)
|
||||
else:
|
||||
txid_hex = swap_client.withdrawCoin(coin_type, value, address, subfee)
|
||||
|
||||
@@ -81,17 +94,22 @@ def js_coins(self, url_split, post_string, is_json) -> bytes:
|
||||
for coin in Coins:
|
||||
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 = {
|
||||
'id': int(coin),
|
||||
'ticker': coin_chainparams['ticker'],
|
||||
'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:
|
||||
entry['variant'] = 'Anon'
|
||||
elif coin == Coins.PART_BLIND:
|
||||
entry['variant'] = 'Blind'
|
||||
elif coin == Coins.LTC_MWEB:
|
||||
entry['variant'] = 'MWEB'
|
||||
coins.append(entry)
|
||||
|
||||
return bytes(json.dumps(coins), 'UTF-8')
|
||||
@@ -108,19 +126,30 @@ def js_wallets(self, url_split, post_string, is_json):
|
||||
cmd = url_split[4]
|
||||
if cmd == 'withdraw':
|
||||
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')
|
||||
if cmd == 'createutxo':
|
||||
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')
|
||||
if cmd == 'reseed':
|
||||
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')
|
||||
|
||||
if coin_type == Coins.LTC_MWEB:
|
||||
coin_type = Coins.LTC
|
||||
rv = swap_client.getWalletInfo(coin_type)
|
||||
rv.update(swap_client.getBlockchainInfo(coin_type))
|
||||
ci = swap_client.ci(coin_type)
|
||||
@@ -199,11 +228,13 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
|
||||
'amount_from': ci_from.format_amount(o.amount_from),
|
||||
'amount_to': ci_to.format_amount((o.amount_from * o.rate) // ci_from.COIN()),
|
||||
'rate': ci_to.format_amount(o.rate),
|
||||
'min_bid_amount': ci_from.format_amount(o.min_bid_amount),
|
||||
'is_expired': o.expire_at <= swap_client.getTime(),
|
||||
'is_own_offer': o.was_sent
|
||||
}
|
||||
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
|
||||
@@ -310,7 +341,10 @@ def js_bids(self, url_split, post_string: str, is_json: bool) -> bytes:
|
||||
extra_options = {
|
||||
'valid_for_seconds': valid_for_seconds,
|
||||
}
|
||||
if have_data_entry(post_data, 'bid_rate'):
|
||||
|
||||
if have_data_entry(post_data, 'amount_to'):
|
||||
extra_options['amount_to'] = inputAmount(get_data_entry(post_data, 'amount_to'), ci_to)
|
||||
elif have_data_entry(post_data, 'bid_rate'):
|
||||
extra_options['bid_rate'] = ci_to.make_int(get_data_entry(post_data, 'bid_rate'), r=1)
|
||||
if have_data_entry(post_data, 'bid_amount'):
|
||||
amount_from = inputAmount(get_data_entry(post_data, 'bid_amount'), ci_from)
|
||||
@@ -396,27 +430,31 @@ def js_smsgaddresses(self, url_split, post_string, is_json) -> bytes:
|
||||
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 url_split[3] == 'new':
|
||||
mode: str = url_split[3]
|
||||
if mode == 'new':
|
||||
addressnote = get_data_entry_or(post_data, 'addressnote', '')
|
||||
new_addr, pubkey = swap_client.newSMSGAddress(addressnote=addressnote)
|
||||
return bytes(json.dumps({'new_address': new_addr, 'pubkey': pubkey}), 'UTF-8')
|
||||
if url_split[3] == 'add':
|
||||
if mode == 'add':
|
||||
addressnote = get_data_entry_or(post_data, 'addressnote', '')
|
||||
pubkey_hex = get_data_entry(post_data, 'addresspubkey')
|
||||
added_address = swap_client.addSMSGAddress(pubkey_hex, addressnote)
|
||||
return bytes(json.dumps({'added_address': added_address, 'pubkey': pubkey_hex}), 'UTF-8')
|
||||
elif url_split[3] == 'edit':
|
||||
elif mode == 'edit':
|
||||
address = get_data_entry(post_data, 'address')
|
||||
activeind = int(get_data_entry(post_data, 'active_ind'))
|
||||
addressnote = get_data_entry_or(post_data, 'addressnote', '')
|
||||
new_addr = swap_client.editSMSGAddress(address, activeind, addressnote)
|
||||
return bytes(json.dumps({'edited_address': address}), 'UTF-8')
|
||||
elif mode == 'disableall':
|
||||
rv = swap_client.disableAllSMSGAddresses()
|
||||
return bytes(json.dumps(rv), 'UTF-8')
|
||||
|
||||
filters = {
|
||||
'exclude_inactive': post_data.get('exclude_inactive', True),
|
||||
}
|
||||
|
||||
return bytes(json.dumps(swap_client.listAllSMSGAddresses(filters)), 'UTF-8')
|
||||
|
||||
|
||||
@@ -464,10 +502,10 @@ def js_rate(self, url_split, post_string, is_json) -> bytes:
|
||||
amount_from = ci_from.format_amount(int((amt_to * rate) // ci_to.COIN()), r=1)
|
||||
return bytes(json.dumps({'amount_from': amount_from}), 'UTF-8')
|
||||
|
||||
amt_from = inputAmount(get_data_entry(post_data, 'amt_from'), ci_from)
|
||||
amt_to = inputAmount(get_data_entry(post_data, 'amt_to'), ci_to)
|
||||
amt_from: int = inputAmount(get_data_entry(post_data, 'amt_from'), ci_from)
|
||||
amt_to: int = inputAmount(get_data_entry(post_data, 'amt_to'), ci_to)
|
||||
|
||||
rate = ci_to.format_amount(ci_from.make_int(amt_to / amt_from, r=1))
|
||||
rate: int = ci_to.format_amount(ci_from.make_int(amt_to / amt_from, r=1))
|
||||
return bytes(json.dumps({'rate': rate}), 'UTF-8')
|
||||
|
||||
|
||||
@@ -603,6 +641,33 @@ def js_automationstrategies(self, url_split, post_string: str, is_json: bool) ->
|
||||
return bytes(json.dumps(rv), 'UTF-8')
|
||||
|
||||
|
||||
def js_validateamount(self, url_split, post_string: str, is_json: bool) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
|
||||
post_data = getFormData(post_string, is_json)
|
||||
|
||||
ticker_str = post_data['coin']
|
||||
amount = post_data['amount']
|
||||
round_method = post_data.get('method', 'none')
|
||||
|
||||
valid_round_methods = ('roundoff', 'rounddown', 'none')
|
||||
if round_method not in valid_round_methods:
|
||||
raise ValueError(f'Unknown rounding method, must be one of {valid_round_methods}')
|
||||
|
||||
coin_type = tickerToCoinId(ticker_str)
|
||||
ci = swap_client.ci(coin_type)
|
||||
|
||||
r = 0
|
||||
if round_method == 'roundoff':
|
||||
r = 1
|
||||
elif round_method == 'rounddown':
|
||||
r = -1
|
||||
|
||||
output_amount = ci.format_amount(amount, conv_int=True, r=r)
|
||||
return bytes(json.dumps(output_amount), 'UTF-8')
|
||||
|
||||
|
||||
def js_vacuumdb(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
@@ -621,15 +686,13 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
|
||||
raise ValueError('Particl wallet seed is set from the Basicswap mnemonic.')
|
||||
|
||||
ci = swap_client.ci(coin)
|
||||
if coin == Coins.XMR:
|
||||
if coin in (Coins.XMR, Coins.WOW):
|
||||
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')
|
||||
|
||||
@@ -647,7 +710,7 @@ def js_setpassword(self, url_split, post_string, is_json) -> bytes:
|
||||
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):
|
||||
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')
|
||||
@@ -701,6 +764,27 @@ def js_help(self, url_split, post_string, is_json) -> bytes:
|
||||
return bytes(json.dumps({'commands': commands}), 'UTF-8')
|
||||
|
||||
|
||||
def js_readurl(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, 'url'):
|
||||
url = get_data_entry(post_data, 'url')
|
||||
default_headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US,en;q=0.5',
|
||||
}
|
||||
response = swap_client.readURL(url, headers=default_headers)
|
||||
try:
|
||||
error = json.loads(response.decode())
|
||||
if "Error" in error:
|
||||
return json.dumps({"Error": error['Error']}).encode()
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return response
|
||||
raise ValueError('Requires URL.')
|
||||
|
||||
|
||||
pages = {
|
||||
'coins': js_coins,
|
||||
'wallets': js_wallets,
|
||||
@@ -718,12 +802,14 @@ pages = {
|
||||
'notifications': js_notifications,
|
||||
'identities': js_identities,
|
||||
'automationstrategies': js_automationstrategies,
|
||||
'validateamount': js_validateamount,
|
||||
'vacuumdb': js_vacuumdb,
|
||||
'getcoinseed': js_getcoinseed,
|
||||
'setpassword': js_setpassword,
|
||||
'unlock': js_unlock,
|
||||
'lock': js_lock,
|
||||
'help': js_help,
|
||||
'readurl': js_readurl,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package basicswap;
|
||||
|
||||
/* Step 1, seller -> network */
|
||||
message OfferMessage {
|
||||
uint32 coin_from = 1;
|
||||
uint32 coin_to = 2;
|
||||
uint64 amount_from = 3;
|
||||
uint64 rate = 4;
|
||||
uint64 min_bid_amount = 5;
|
||||
uint64 time_valid = 6;
|
||||
enum LockType {
|
||||
NOT_SET = 0;
|
||||
SEQUENCE_LOCK_BLOCKS = 1;
|
||||
SEQUENCE_LOCK_TIME = 2;
|
||||
ABS_LOCK_BLOCKS = 3;
|
||||
ABS_LOCK_TIME = 4;
|
||||
}
|
||||
LockType lock_type = 7;
|
||||
uint32 lock_value = 8;
|
||||
uint32 swap_type = 9;
|
||||
|
||||
/* optional */
|
||||
string proof_address = 10;
|
||||
string proof_signature = 11;
|
||||
bytes pkhash_seller = 12;
|
||||
bytes secret_hash = 13;
|
||||
|
||||
uint64 fee_rate_from = 14;
|
||||
uint64 fee_rate_to = 15;
|
||||
|
||||
uint32 protocol_version = 16;
|
||||
bool amount_negotiable = 17;
|
||||
bool rate_negotiable = 18;
|
||||
}
|
||||
|
||||
/* Step 2, buyer -> seller */
|
||||
message BidMessage {
|
||||
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;
|
||||
bytes pkhash_buyer = 5; /* buyer's address to receive amount_from */
|
||||
string proof_address = 6;
|
||||
string proof_signature = 7;
|
||||
|
||||
uint32 protocol_version = 8;
|
||||
}
|
||||
|
||||
/* Step 3, seller -> buyer */
|
||||
message BidAcceptMessage {
|
||||
bytes bid_msg_id = 1;
|
||||
bytes initiate_txid = 2;
|
||||
bytes contract_script = 3;
|
||||
}
|
||||
|
||||
message OfferRevokeMessage {
|
||||
bytes offer_msg_id = 1;
|
||||
bytes signature = 2;
|
||||
}
|
||||
|
||||
message BidRejectMessage {
|
||||
bytes bid_msg_id = 1;
|
||||
|
||||
uint32 reject_code = 2;
|
||||
}
|
||||
|
||||
message XmrBidMessage {
|
||||
/* MSG1L, F -> L */
|
||||
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;
|
||||
|
||||
bytes pkaf = 5;
|
||||
|
||||
bytes kbvf = 6;
|
||||
bytes kbsf_dleag = 7;
|
||||
|
||||
bytes dest_af = 8;
|
||||
|
||||
uint32 protocol_version = 9;
|
||||
}
|
||||
|
||||
message XmrSplitMessage {
|
||||
bytes msg_id = 1;
|
||||
uint32 msg_type = 2; /* 1 XmrBid, 2 XmrBidAccept */
|
||||
uint32 sequence = 3;
|
||||
bytes dleag = 4;
|
||||
}
|
||||
|
||||
message XmrBidAcceptMessage {
|
||||
bytes bid_msg_id = 1;
|
||||
|
||||
bytes pkal = 3;
|
||||
|
||||
bytes kbvl = 4;
|
||||
bytes kbsl_dleag = 5;
|
||||
|
||||
/* MSG2F */
|
||||
bytes a_lock_tx = 6;
|
||||
bytes a_lock_tx_script = 7;
|
||||
bytes a_lock_refund_tx = 8;
|
||||
bytes a_lock_refund_tx_script = 9;
|
||||
bytes a_lock_refund_spend_tx = 10;
|
||||
bytes al_lock_refund_tx_sig = 11;
|
||||
}
|
||||
|
||||
message XmrBidLockTxSigsMessage {
|
||||
/* MSG3L */
|
||||
bytes bid_msg_id = 1;
|
||||
|
||||
bytes af_lock_refund_spend_tx_esig = 2;
|
||||
bytes af_lock_refund_tx_sig = 3;
|
||||
}
|
||||
|
||||
message XmrBidLockSpendTxMessage {
|
||||
/* MSG4F */
|
||||
bytes bid_msg_id = 1;
|
||||
|
||||
bytes a_lock_spend_tx = 2;
|
||||
bytes kal_sig = 3;
|
||||
}
|
||||
|
||||
message XmrBidLockReleaseMessage {
|
||||
/* MSG5F */
|
||||
bytes bid_msg_id = 1;
|
||||
|
||||
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;
|
||||
}
|
||||
265
basicswap/messages_npb.py
Normal file
@@ -0,0 +1,265 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2024 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
|
||||
'''
|
||||
syntax = "proto3";
|
||||
|
||||
0 VARINT int32, int64, uint32, uint64, sint32, sint64, bool, enum
|
||||
1 I64 fixed64, sfixed64, double
|
||||
2 LEN string, bytes, embedded messages, packed repeated fields
|
||||
5 I32 fixed32, sfixed32, float
|
||||
|
||||
Don't encode fields of default values.
|
||||
When decoding initialise all fields not set from data.
|
||||
|
||||
protobuf ParseFromString would reset the whole object, from_bytes won't.
|
||||
'''
|
||||
|
||||
from basicswap.util.integer import encode_varint, decode_varint
|
||||
|
||||
|
||||
class NonProtobufClass():
|
||||
def __init__(self, init_all: bool = True, **kwargs):
|
||||
for key, value in kwargs.items():
|
||||
found_field: bool = False
|
||||
for field_num, v in self._map.items():
|
||||
field_name, wire_type, field_type = v
|
||||
if field_name == key:
|
||||
setattr(self, field_name, value)
|
||||
found_field = True
|
||||
break
|
||||
if found_field is False:
|
||||
raise ValueError(f'got an unexpected keyword argument \'{key}\'')
|
||||
|
||||
if init_all:
|
||||
self.init_fields()
|
||||
|
||||
def init_fields(self) -> None:
|
||||
# Set default values for missing fields
|
||||
for field_num, v in self._map.items():
|
||||
field_name, wire_type, field_type = v
|
||||
if hasattr(self, field_name):
|
||||
continue
|
||||
if wire_type == 0:
|
||||
setattr(self, field_name, 0)
|
||||
elif wire_type == 2:
|
||||
if field_type == 1:
|
||||
setattr(self, field_name, str())
|
||||
else:
|
||||
setattr(self, field_name, bytes())
|
||||
else:
|
||||
raise ValueError(f'Unknown wire_type {wire_type}')
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
rv = bytes()
|
||||
|
||||
for field_num, v in self._map.items():
|
||||
field_name, wire_type, field_type = v
|
||||
if not hasattr(self, field_name):
|
||||
continue
|
||||
field_value = getattr(self, field_name)
|
||||
tag = (field_num << 3) | wire_type
|
||||
if wire_type == 0:
|
||||
if field_value == 0:
|
||||
continue
|
||||
rv += encode_varint(tag)
|
||||
rv += encode_varint(field_value)
|
||||
elif wire_type == 2:
|
||||
if len(field_value) == 0:
|
||||
continue
|
||||
rv += encode_varint(tag)
|
||||
if isinstance(field_value, str):
|
||||
field_value = field_value.encode('utf-8')
|
||||
rv += encode_varint(len(field_value))
|
||||
rv += field_value
|
||||
else:
|
||||
raise ValueError(f'Unknown wire_type {wire_type}')
|
||||
return rv
|
||||
|
||||
def from_bytes(self, b: bytes, init_all: bool = True) -> None:
|
||||
max_len: int = len(b)
|
||||
o: int = 0
|
||||
while o < max_len:
|
||||
tag, lv = decode_varint(b, o)
|
||||
o += lv
|
||||
wire_type = tag & 7
|
||||
field_num = tag >> 3
|
||||
|
||||
field_name, wire_type_expect, field_type = self._map[field_num]
|
||||
if wire_type != wire_type_expect:
|
||||
raise ValueError(f'Unexpected wire_type {wire_type} for field {field_num}')
|
||||
|
||||
if wire_type == 0:
|
||||
field_value, lv = decode_varint(b, o)
|
||||
o += lv
|
||||
elif wire_type == 2:
|
||||
field_len, lv = decode_varint(b, o)
|
||||
o += lv
|
||||
field_value = b[o: o + field_len]
|
||||
o += field_len
|
||||
if field_type == 1:
|
||||
field_value = field_value.decode('utf-8')
|
||||
else:
|
||||
raise ValueError(f'Unknown wire_type {wire_type}')
|
||||
|
||||
setattr(self, field_name, field_value)
|
||||
|
||||
if init_all:
|
||||
self.init_fields()
|
||||
|
||||
|
||||
class OfferMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ('protocol_version', 0, 0),
|
||||
2: ('coin_from', 0, 0),
|
||||
3: ('coin_to', 0, 0),
|
||||
4: ('amount_from', 0, 0),
|
||||
5: ('amount_to', 0, 0),
|
||||
6: ('min_bid_amount', 0, 0),
|
||||
7: ('time_valid', 0, 0),
|
||||
8: ('lock_type', 0, 0),
|
||||
9: ('lock_value', 0, 0),
|
||||
10: ('swap_type', 0, 0),
|
||||
11: ('proof_address', 2, 1),
|
||||
12: ('proof_signature', 2, 1),
|
||||
13: ('pkhash_seller', 2, 0),
|
||||
14: ('secret_hash', 2, 0),
|
||||
15: ('fee_rate_from', 0, 0),
|
||||
16: ('fee_rate_to', 0, 0),
|
||||
17: ('amount_negotiable', 0, 2),
|
||||
18: ('rate_negotiable', 0, 2),
|
||||
19: ('proof_utxos', 2, 0),
|
||||
}
|
||||
|
||||
|
||||
class BidMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ('protocol_version', 0, 0),
|
||||
2: ('offer_msg_id', 2, 0),
|
||||
3: ('time_valid', 0, 0),
|
||||
4: ('amount', 0, 0),
|
||||
5: ('amount_to', 0, 0),
|
||||
6: ('pkhash_buyer', 2, 0),
|
||||
7: ('proof_address', 2, 1),
|
||||
8: ('proof_signature', 2, 1),
|
||||
9: ('proof_utxos', 2, 0),
|
||||
10: ('pkhash_buyer_to', 2, 0),
|
||||
}
|
||||
|
||||
|
||||
class BidAcceptMessage(NonProtobufClass):
|
||||
# Step 3, seller -> buyer
|
||||
_map = {
|
||||
1: ('bid_msg_id', 2, 0),
|
||||
2: ('initiate_txid', 2, 0),
|
||||
3: ('contract_script', 2, 0),
|
||||
4: ('pkhash_seller', 2, 0),
|
||||
}
|
||||
|
||||
|
||||
class OfferRevokeMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ('offer_msg_id', 2, 0),
|
||||
2: ('signature', 2, 0),
|
||||
}
|
||||
|
||||
|
||||
class BidRejectMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ('bid_msg_id', 2, 0),
|
||||
2: ('reject_code', 0, 0),
|
||||
}
|
||||
|
||||
|
||||
class XmrBidMessage(NonProtobufClass):
|
||||
# MSG1L, F -> L
|
||||
_map = {
|
||||
1: ('protocol_version', 0, 0),
|
||||
2: ('offer_msg_id', 2, 0),
|
||||
3: ('time_valid', 0, 0),
|
||||
4: ('amount', 0, 0),
|
||||
5: ('amount_to', 0, 0),
|
||||
6: ('pkaf', 2, 0),
|
||||
7: ('kbvf', 2, 0),
|
||||
8: ('kbsf_dleag', 2, 0),
|
||||
9: ('dest_af', 2, 0),
|
||||
}
|
||||
|
||||
|
||||
class XmrSplitMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ('msg_id', 2, 0),
|
||||
2: ('msg_type', 0, 0),
|
||||
3: ('sequence', 0, 0),
|
||||
4: ('dleag', 2, 0),
|
||||
}
|
||||
|
||||
|
||||
class XmrBidAcceptMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ('bid_msg_id', 2, 0),
|
||||
2: ('pkal', 2, 0),
|
||||
3: ('kbvl', 2, 0),
|
||||
4: ('kbsl_dleag', 2, 0),
|
||||
|
||||
# MSG2F
|
||||
5: ('a_lock_tx', 2, 0),
|
||||
6: ('a_lock_tx_script', 2, 0),
|
||||
7: ('a_lock_refund_tx', 2, 0),
|
||||
8: ('a_lock_refund_tx_script', 2, 0),
|
||||
9: ('a_lock_refund_spend_tx', 2, 0),
|
||||
10: ('al_lock_refund_tx_sig', 2, 0),
|
||||
}
|
||||
|
||||
|
||||
class XmrBidLockTxSigsMessage(NonProtobufClass):
|
||||
# MSG3L
|
||||
_map = {
|
||||
1: ('bid_msg_id', 2, 0),
|
||||
2: ('af_lock_refund_spend_tx_esig', 2, 0),
|
||||
3: ('af_lock_refund_tx_sig', 2, 0),
|
||||
}
|
||||
|
||||
|
||||
class XmrBidLockSpendTxMessage(NonProtobufClass):
|
||||
# MSG4F
|
||||
_map = {
|
||||
1: ('bid_msg_id', 2, 0),
|
||||
2: ('a_lock_spend_tx', 2, 0),
|
||||
3: ('kal_sig', 2, 0),
|
||||
}
|
||||
|
||||
|
||||
class XmrBidLockReleaseMessage(NonProtobufClass):
|
||||
# MSG5F
|
||||
_map = {
|
||||
1: ('bid_msg_id', 2, 0),
|
||||
2: ('al_lock_spend_tx_esig', 2, 0),
|
||||
}
|
||||
|
||||
|
||||
class ADSBidIntentMessage(NonProtobufClass):
|
||||
# L -> F Sent from bidder, construct a reverse bid
|
||||
_map = {
|
||||
1: ('protocol_version', 0, 0),
|
||||
2: ('offer_msg_id', 2, 0),
|
||||
3: ('time_valid', 0, 0),
|
||||
4: ('amount_from', 0, 0),
|
||||
5: ('amount_to', 0, 0),
|
||||
}
|
||||
|
||||
|
||||
class ADSBidIntentAcceptMessage(NonProtobufClass):
|
||||
# F -> L Sent from offerer, construct a reverse bid
|
||||
_map = {
|
||||
1: ('bid_msg_id', 2, 0),
|
||||
2: ('pkaf', 2, 0),
|
||||
3: ('kbvf', 2, 0),
|
||||
4: ('kbsf_dleag', 2, 0),
|
||||
5: ('dest_af', 2, 0),
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: messages.proto
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf.internal import builder as _builder
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_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\"\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())
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'messages_pb2', globals())
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
_OFFERMESSAGE._serialized_start=30
|
||||
_OFFERMESSAGE._serialized_end=580
|
||||
_OFFERMESSAGE_LOCKTYPE._serialized_start=467
|
||||
_OFFERMESSAGE_LOCKTYPE._serialized_end=580
|
||||
_BIDMESSAGE._serialized_start=583
|
||||
_BIDMESSAGE._serialized_end=763
|
||||
_BIDACCEPTMESSAGE._serialized_start=765
|
||||
_BIDACCEPTMESSAGE._serialized_end=851
|
||||
_OFFERREVOKEMESSAGE._serialized_start=853
|
||||
_OFFERREVOKEMESSAGE._serialized_end=914
|
||||
_BIDREJECTMESSAGE._serialized_start=916
|
||||
_BIDREJECTMESSAGE._serialized_end=975
|
||||
_XMRBIDMESSAGE._serialized_start=978
|
||||
_XMRBIDMESSAGE._serialized_end=1156
|
||||
_XMRSPLITMESSAGE._serialized_start=1158
|
||||
_XMRSPLITMESSAGE._serialized_end=1242
|
||||
_XMRBIDACCEPTMESSAGE._serialized_start=1245
|
||||
_XMRBIDACCEPTMESSAGE._serialized_end=1501
|
||||
_XMRBIDLOCKTXSIGSMESSAGE._serialized_start=1503
|
||||
_XMRBIDLOCKTXSIGSMESSAGE._serialized_end=1617
|
||||
_XMRBIDLOCKSPENDTXMESSAGE._serialized_start=1619
|
||||
_XMRBIDLOCKSPENDTXMESSAGE._serialized_end=1707
|
||||
_XMRBIDLOCKRELEASEMESSAGE._serialized_start=1709
|
||||
_XMRBIDLOCKRELEASEMESSAGE._serialized_end=1786
|
||||
_ADSBIDINTENTMESSAGE._serialized_start=1789
|
||||
_ADSBIDINTENTMESSAGE._serialized_end=1932
|
||||
_ADSBIDINTENTACCEPTMESSAGE._serialized_start=1934
|
||||
_ADSBIDINTENTACCEPTMESSAGE._serialized_end=2046
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
@@ -24,7 +24,6 @@ import queue
|
||||
import random
|
||||
import select
|
||||
import socket
|
||||
import struct
|
||||
import hashlib
|
||||
import logging
|
||||
import secrets
|
||||
@@ -41,7 +40,7 @@ from basicswap.contrib.rfc6979 import (
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -83,8 +82,8 @@ class MsgHandshake:
|
||||
pass
|
||||
|
||||
def encode_aad(self): # Additional Authenticated Data
|
||||
return struct.pack('>H', NetMessageTypes.HANDSHAKE) + \
|
||||
struct.pack('>Q', self._timestamp) + \
|
||||
return int(NetMessageTypes.HANDSHAKE).to_bytes(2, 'big') + \
|
||||
self._timestamp.to_bytes(8, 'big') + \
|
||||
self._ephem_pk
|
||||
|
||||
def encode(self):
|
||||
@@ -92,7 +91,7 @@ class MsgHandshake:
|
||||
|
||||
def decode(self, msg_mv):
|
||||
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
|
||||
self._ephem_pk = bytes(msg_mv[o: o + 33])
|
||||
o += 33
|
||||
@@ -333,7 +332,7 @@ class Network:
|
||||
|
||||
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._km = hashed[32:]
|
||||
|
||||
@@ -386,7 +385,7 @@ class Network:
|
||||
nk = PrivateKey(self._network_key)
|
||||
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._km = hashed[32:]
|
||||
|
||||
@@ -427,7 +426,7 @@ class Network:
|
||||
mac = msg_mv[-16:]
|
||||
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
|
||||
if len(plaintext) >= 10:
|
||||
peer._ready = True
|
||||
@@ -450,7 +449,7 @@ class Network:
|
||||
mac = msg_mv[-16:]
|
||||
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:
|
||||
peer._last_ping_rtt = (time.time_ns() // 1000) - peer._last_ping_at
|
||||
@@ -462,14 +461,14 @@ class Network:
|
||||
def send_ping(self, peer):
|
||||
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]
|
||||
|
||||
cipher = ChaCha20_Poly1305.new(key=peer._ke, nonce=nonce)
|
||||
cipher.update(msg_bytes)
|
||||
cipher.update(nonce)
|
||||
|
||||
payload = struct.pack('>I', ping_nonce)
|
||||
payload = ping_nonce.to_bytes(4, 'big')
|
||||
if peer._last_ping_at == 0:
|
||||
payload += self._sc._version
|
||||
ct, mac = cipher.encrypt_and_digest(payload)
|
||||
@@ -484,14 +483,14 @@ class Network:
|
||||
self.send_msg(peer, msg_bytes)
|
||||
|
||||
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]
|
||||
|
||||
cipher = ChaCha20_Poly1305.new(key=peer._ke, nonce=nonce)
|
||||
cipher.update(msg_bytes)
|
||||
cipher.update(nonce)
|
||||
|
||||
payload = struct.pack('>I', ping_nonce)
|
||||
payload = ping_nonce.to_bytes(4, 'big')
|
||||
ct, mac = cipher.encrypt_and_digest(payload)
|
||||
msg_bytes += ct + mac
|
||||
|
||||
@@ -503,7 +502,7 @@ class Network:
|
||||
msg_encoded = msg if isinstance(msg, bytes) else msg.encode()
|
||||
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._bytes_sent += len_encoded
|
||||
@@ -515,7 +514,7 @@ class Network:
|
||||
try:
|
||||
mv = memoryview(msg_bytes)
|
||||
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:
|
||||
self.process_handshake(peer, mv)
|
||||
elif msg_type == NetMessageTypes.PING:
|
||||
@@ -548,13 +547,13 @@ class Network:
|
||||
raise ValueError('Invalid start token')
|
||||
o += 2
|
||||
|
||||
msg_len = struct.unpack('>I', mv[o: o + 4])[0]
|
||||
msg_len = int.from_bytes(mv[o: o + 4], 'big')
|
||||
o += 4
|
||||
if msg_len < 2 or msg_len > MSG_MAX_SIZE:
|
||||
raise ValueError('Invalid data length')
|
||||
|
||||
# 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
|
||||
if not NetMessageTypes.has_value(msg_type):
|
||||
raise ValueError('Invalid msg type')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2020-2023 tecnovert
|
||||
# Copyright (c) 2020-2024 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -10,12 +10,15 @@ from basicswap.db import (
|
||||
from basicswap.util import (
|
||||
SerialiseNum,
|
||||
)
|
||||
from basicswap.util.script import (
|
||||
decodeScriptNum,
|
||||
)
|
||||
from basicswap.script import (
|
||||
OpCodes,
|
||||
)
|
||||
from basicswap.basicswap_util import (
|
||||
SwapTypes,
|
||||
EventLogTypes,
|
||||
SwapTypes,
|
||||
)
|
||||
from . import ProtocolInterface
|
||||
|
||||
@@ -23,13 +26,13 @@ INITIATE_TX_TIMEOUT = 40 * 60 # TODO: make variable per coin
|
||||
ABS_LOCK_TIME_LEEWAY = 10 * 60
|
||||
|
||||
|
||||
def buildContractScript(lock_val: int, secret_hash: bytes, pkh_redeem: bytes, pkh_refund: bytes, op_lock=OpCodes.OP_CHECKSEQUENCEVERIFY) -> bytearray:
|
||||
def buildContractScript(lock_val: int, secret_hash: bytes, pkh_redeem: bytes, pkh_refund: bytes, op_lock=OpCodes.OP_CHECKSEQUENCEVERIFY, op_hash=OpCodes.OP_SHA256) -> bytearray:
|
||||
script = bytearray([
|
||||
OpCodes.OP_IF,
|
||||
OpCodes.OP_SIZE,
|
||||
0x01, 0x20, # 32
|
||||
OpCodes.OP_EQUALVERIFY,
|
||||
OpCodes.OP_SHA256,
|
||||
op_hash,
|
||||
0x20]) \
|
||||
+ secret_hash \
|
||||
+ bytearray([
|
||||
@@ -54,6 +57,46 @@ def buildContractScript(lock_val: int, secret_hash: bytes, pkh_redeem: bytes, pk
|
||||
return script
|
||||
|
||||
|
||||
def verifyContractScript(script, op_lock=OpCodes.OP_CHECKSEQUENCEVERIFY, op_hash=OpCodes.OP_SHA256):
|
||||
if script[0] != OpCodes.OP_IF or \
|
||||
script[1] != OpCodes.OP_SIZE or \
|
||||
script[2] != 0x01 or script[3] != 0x20 or \
|
||||
script[4] != OpCodes.OP_EQUALVERIFY or \
|
||||
script[5] != op_hash or \
|
||||
script[6] != 0x20:
|
||||
return False, None, None, None, None
|
||||
o = 7
|
||||
script_hash = script[o: o + 32]
|
||||
o += 32
|
||||
if script[o] != OpCodes.OP_EQUALVERIFY or \
|
||||
script[o + 1] != OpCodes.OP_DUP or \
|
||||
script[o + 2] != OpCodes.OP_HASH160 or \
|
||||
script[o + 3] != 0x14:
|
||||
return False, script_hash, None, None, None
|
||||
o += 4
|
||||
pkh_redeem = script[o: o + 20]
|
||||
o += 20
|
||||
if script[o] != OpCodes.OP_ELSE:
|
||||
return False, script_hash, pkh_redeem, None, None
|
||||
o += 1
|
||||
lock_val, nb = decodeScriptNum(script, o)
|
||||
o += nb
|
||||
if script[o] != op_lock or \
|
||||
script[o + 1] != OpCodes.OP_DROP or \
|
||||
script[o + 2] != OpCodes.OP_DUP or \
|
||||
script[o + 3] != OpCodes.OP_HASH160 or \
|
||||
script[o + 4] != 0x14:
|
||||
return False, script_hash, pkh_redeem, lock_val, None
|
||||
o += 5
|
||||
pkh_refund = script[o: o + 20]
|
||||
o += 20
|
||||
if script[o] != OpCodes.OP_ENDIF or \
|
||||
script[o + 1] != OpCodes.OP_EQUALVERIFY or \
|
||||
script[o + 2] != OpCodes.OP_CHECKSIG:
|
||||
return False, script_hash, pkh_redeem, lock_val, pkh_refund
|
||||
return True, script_hash, pkh_redeem, lock_val, pkh_refund
|
||||
|
||||
|
||||
def extractScriptSecretHash(script):
|
||||
return script[7:39]
|
||||
|
||||
@@ -62,7 +105,7 @@ def redeemITx(self, bid_id: bytes, session):
|
||||
bid, offer = self.getBidAndOffer(bid_id, session)
|
||||
ci_from = self.ci(offer.coin_from)
|
||||
|
||||
txn = self.createRedeemTxn(ci_from.coin_type(), bid, for_txn_type='initiate')
|
||||
txn = self.createRedeemTxn(ci_from.coin_type(), bid, for_txn_type='initiate', session=session)
|
||||
txid = ci_from.publishTx(bytes.fromhex(txn))
|
||||
|
||||
bid.initiate_tx.spend_txid = bytes.fromhex(txid)
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2020-2023 tecnovert
|
||||
# Copyright (c) 2020-2024 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
from sqlalchemy.orm import scoped_session
|
||||
|
||||
from basicswap.util import (
|
||||
ensure,
|
||||
)
|
||||
from basicswap.interface import Curves
|
||||
from basicswap.interface.base import Curves
|
||||
from basicswap.chainparams import (
|
||||
Coins,
|
||||
)
|
||||
@@ -21,13 +19,17 @@ from basicswap.basicswap_util import (
|
||||
from . import ProtocolInterface
|
||||
from basicswap.contrib.test_framework.script import (
|
||||
CScript, CScriptOp,
|
||||
OP_CHECKMULTISIG)
|
||||
OP_CHECKMULTISIG
|
||||
)
|
||||
|
||||
|
||||
def addLockRefundSigs(self, xmr_swap, ci):
|
||||
self.log.debug('Setting lock refund tx sigs')
|
||||
witness_stack = [
|
||||
b'',
|
||||
|
||||
witness_stack = []
|
||||
if ci.coin_type() not in (Coins.DCR, ):
|
||||
witness_stack += [b'', ]
|
||||
witness_stack += [
|
||||
xmr_swap.al_lock_refund_tx_sig,
|
||||
xmr_swap.af_lock_refund_tx_sig,
|
||||
xmr_swap.a_lock_tx_script,
|
||||
@@ -41,7 +43,7 @@ def addLockRefundSigs(self, xmr_swap, ci):
|
||||
def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key):
|
||||
self.log.info('Manually recovering %s', bid_id.hex())
|
||||
# Manually recover txn if other key is known
|
||||
session = scoped_session(self.session_factory)
|
||||
session = self.openSession()
|
||||
try:
|
||||
bid, xmr_swap = self.getXmrBidFromSession(session, bid_id)
|
||||
ensure(bid, 'Bid not found: {}.'.format(bid_id.hex()))
|
||||
@@ -74,15 +76,15 @@ def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key):
|
||||
address_to = self.getCachedStealthAddressForCoin(offer.coin_to)
|
||||
|
||||
amount = bid.amount_to
|
||||
txid = ci_to.spendBLockTx(xmr_swap.b_lock_tx_id, address_to, xmr_swap.vkbv, vkbs, bid.amount_to, xmr_offer.b_fee_rate, bid.chain_b_height_start, spend_actual_balance=True)
|
||||
lock_tx_vout = bid.getLockTXBVout()
|
||||
txid = ci_to.spendBLockTx(xmr_swap.b_lock_tx_id, address_to, xmr_swap.vkbv, vkbs, amount, xmr_offer.b_fee_rate, bid.chain_b_height_start, spend_actual_balance=True, lock_tx_vout=lock_tx_vout)
|
||||
self.log.debug('Submitted lock B spend txn %s to %s chain for bid %s', txid.hex(), ci_to.coin_name(), bid_id.hex())
|
||||
self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_B_SPEND_TX_PUBLISHED, txid.hex(), session)
|
||||
session.commit()
|
||||
|
||||
return txid
|
||||
finally:
|
||||
session.close()
|
||||
session.remove()
|
||||
self.closeSession(session, commit=False)
|
||||
|
||||
|
||||
def getChainBSplitKey(swap_client, bid, xmr_swap, offer):
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2020-2023 tecnovert
|
||||
# Copyright (c) 2020-2024 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import os
|
||||
import time
|
||||
import json
|
||||
import shlex
|
||||
import urllib
|
||||
import logging
|
||||
import traceback
|
||||
import subprocess
|
||||
from xmlrpc.client import (
|
||||
@@ -20,21 +18,6 @@ from xmlrpc.client import (
|
||||
from .util import jsonDecimal
|
||||
|
||||
|
||||
def waitForRPC(rpc_func, expect_wallet=True, max_tries=7):
|
||||
for i in range(max_tries + 1):
|
||||
try:
|
||||
if expect_wallet:
|
||||
rpc_func('getwalletinfo')
|
||||
else:
|
||||
rpc_func('getblockchaininfo')
|
||||
return
|
||||
except Exception as ex:
|
||||
if i < max_tries:
|
||||
logging.warning('Can\'t connect to RPC: %s. Retrying in %d second/s.', str(ex), (i + 1))
|
||||
time.sleep(i + 1)
|
||||
raise ValueError('waitForRPC failed')
|
||||
|
||||
|
||||
class Jsonrpc():
|
||||
# __getattr__ complicates extending ServerProxy
|
||||
def __init__(self, uri, transport=None, encoding=None, verbose=False,
|
||||
@@ -111,7 +94,7 @@ def callrpc(rpc_port, auth, method, params=[], wallet=None, host='127.0.0.1'):
|
||||
r = json.loads(v.decode('utf-8'))
|
||||
except Exception as ex:
|
||||
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:
|
||||
raise ValueError('RPC error ' + str(r['error']))
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import os
|
||||
import json
|
||||
import socks
|
||||
import time
|
||||
import urllib
|
||||
import hashlib
|
||||
@@ -10,9 +11,32 @@ from xmlrpc.client import (
|
||||
Transport,
|
||||
SafeTransport,
|
||||
)
|
||||
from sockshandler import SocksiPyConnection
|
||||
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():
|
||||
# __getattr__ complicates extending ServerProxy
|
||||
def __init__(self, uri, transport=None, encoding=None, verbose=False,
|
||||
@@ -148,7 +172,7 @@ class JsonrpcDigest():
|
||||
raise
|
||||
|
||||
|
||||
def callrpc_xmr(rpc_port, method, params=[], rpc_host='127.0.0.1', path='json_rpc', auth=None, 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)
|
||||
try:
|
||||
if rpc_host.count('://') > 0:
|
||||
@@ -156,7 +180,7 @@ def callrpc_xmr(rpc_port, method, params=[], rpc_host='127.0.0.1', path='json_rp
|
||||
else:
|
||||
url = 'http://{}:{}/{}'.format(rpc_host, rpc_port, path)
|
||||
|
||||
x = JsonrpcDigest(url)
|
||||
x = JsonrpcDigest(url, transport=transport)
|
||||
request_body = {
|
||||
'method': method,
|
||||
'params': params,
|
||||
@@ -170,22 +194,22 @@ def callrpc_xmr(rpc_port, method, params=[], rpc_host='127.0.0.1', path='json_rp
|
||||
x.close()
|
||||
r = json.loads(v.decode('utf-8'))
|
||||
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:
|
||||
raise ValueError('RPC error ' + str(r['error']))
|
||||
raise ValueError(tag + 'RPC error ' + str(r['error']))
|
||||
|
||||
return r['result']
|
||||
|
||||
|
||||
def callrpc_xmr2(rpc_port: int, method: str, params=None, auth=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:
|
||||
if rpc_host.count('://') > 0:
|
||||
url = '{}:{}/{}'.format(rpc_host, rpc_port, method)
|
||||
else:
|
||||
url = 'http://{}:{}/{}'.format(rpc_host, rpc_port, method)
|
||||
|
||||
x = JsonrpcDigest(url)
|
||||
x = JsonrpcDigest(url, transport=transport)
|
||||
if auth:
|
||||
v = x.json_request(params, username=auth[0], password=auth[1], timeout=timeout)
|
||||
else:
|
||||
@@ -193,28 +217,42 @@ def callrpc_xmr2(rpc_port: int, method: str, params=None, auth=None, rpc_host='1
|
||||
x.close()
|
||||
r = json.loads(v.decode('utf-8'))
|
||||
except Exception as ex:
|
||||
raise ValueError('RPC Server Error: {}'.format(str(ex)))
|
||||
raise ValueError('{}RPC Server Error: {}'.format(tag, str(ex)))
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def make_xmr_rpc2_func(port, auth, 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
|
||||
auth = auth
|
||||
host = host
|
||||
transport = None
|
||||
default_timeout = default_timeout
|
||||
tag = tag
|
||||
|
||||
def rpc_func(method, params=None, wallet=None, timeout=120):
|
||||
nonlocal port, auth, host
|
||||
return callrpc_xmr2(port, method, params, auth=auth, rpc_host=host, timeout=timeout)
|
||||
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_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'):
|
||||
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
|
||||
|
||||
def rpc_func(method, params=None, wallet=None, timeout=120):
|
||||
nonlocal port, auth, host
|
||||
return callrpc_xmr(port, method, params, rpc_host=host, auth=auth, timeout=timeout)
|
||||
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
|
||||
|
||||
@@ -26,3 +26,5 @@ class OpCodes(IntEnum):
|
||||
OP_CHECKSIG = 0xac,
|
||||
OP_CHECKLOCKTIMEVERIFY = 0xb1,
|
||||
OP_CHECKSEQUENCEVERIFY = 0xb2,
|
||||
|
||||
OP_SHA256_DECRED = 0xc0,
|
||||
|
||||
@@ -1,65 +1,63 @@
|
||||
|
||||
.padded_row td
|
||||
{
|
||||
padding-top:1.5em;
|
||||
/* General Styles */
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.bold
|
||||
{
|
||||
font-weight:bold;
|
||||
.monospace {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.monospace
|
||||
{
|
||||
font-family:monospace;
|
||||
}
|
||||
|
||||
.floatright
|
||||
{
|
||||
.floatright {
|
||||
position: fixed;
|
||||
top:1.25rem;
|
||||
right:1.25rem;
|
||||
top: 1.25rem;
|
||||
right: 1.25rem;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.error_msg
|
||||
{
|
||||
color:red;
|
||||
/* Table Styles */
|
||||
.padded_row td {
|
||||
padding-top: 1.5em;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal-highest {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* Animation */
|
||||
#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;
|
||||
}
|
||||
-moz-animation: cssAnimation 0s ease-in 15s forwards;
|
||||
-webkit-animation: cssAnimation 0s ease-in 15s forwards;
|
||||
-o-animation: cssAnimation 0s ease-in 15s forwards;
|
||||
animation: cssAnimation 0s ease-in 15s forwards;
|
||||
-webkit-animation-fill-mode: forwards;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
.custom-select .select {
|
||||
appearance: none;
|
||||
background-image: url('/static/images/other/coin.png');
|
||||
background-position: 10px center;
|
||||
background-repeat: no-repeat;
|
||||
position: relative;
|
||||
@keyframes cssAnimation {
|
||||
to {
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes cssAnimation {
|
||||
to {
|
||||
width: 0;
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom Select Styles */
|
||||
.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;
|
||||
@@ -96,8 +94,20 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Blur and Overlay Styles */
|
||||
.blurred {
|
||||
filter: blur(3px);
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Disable opacity on disabled form elements in Chrome */
|
||||
.error-overlay.non-blurred {
|
||||
filter: none;
|
||||
pointer-events: auto;
|
||||
user-select: auto;
|
||||
}
|
||||
|
||||
/* Form Element Styles */
|
||||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||
select:disabled,
|
||||
input:disabled,
|
||||
@@ -106,14 +116,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Add this to your existing CSS file */
|
||||
.error {
|
||||
border: 1px solid red !important;
|
||||
}
|
||||
|
||||
/* Active Container Styles */
|
||||
.active-container {
|
||||
position: relative;
|
||||
border-radius: 5px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.active-container::before {
|
||||
@@ -128,3 +138,222 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Center Spin Animation */
|
||||
.center-spin {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Hover Container Styles */
|
||||
.hover-container:hover #coin_to_button,
|
||||
.hover-container:hover #coin_to,
|
||||
.hover-container:hover #coin_from_button,
|
||||
.hover-container:hover #coin_from {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
#coin_to_button, #coin_from_button {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
|
||||
/* Input-like Container Styles */
|
||||
.input-like-container {
|
||||
max-width: 100%;
|
||||
background-color: #ffffff;
|
||||
width: 360px;
|
||||
padding: 1rem;
|
||||
color: #374151;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
outline: none;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-all;
|
||||
height: auto;
|
||||
min-height: 90px;
|
||||
max-height: 150px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.input-like-container.dark {
|
||||
background-color: #374151;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.input-like-container.copying {
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
/* QR Code Styles */
|
||||
.qrcode {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.qrcode-border {
|
||||
border: 2px solid;
|
||||
background-color: #ffffff;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
.qrcode img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
#showQR {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.qrcode-container {
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
/* Disabled Element Styles */
|
||||
select.select-disabled,
|
||||
.disabled-input-enabled,
|
||||
select.disabled-select-enabled {
|
||||
opacity: 0.40 !important;
|
||||
}
|
||||
|
||||
/* Shutdown Modal Styles */
|
||||
#shutdownModal {
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
#shutdownModal > div:first-child {
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
#shutdownModal > div:last-child {
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
#shutdownModal > div {
|
||||
transition: opacity 0.3s ease-out;
|
||||
}
|
||||
|
||||
#shutdownModal.hidden > div {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#shutdownModal:not(.hidden) > div {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.shutdown-button {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.shutdown-button.shutdown-disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
.shutdown-button.shutdown-disabled:hover {
|
||||
background-color: #4a5568;
|
||||
}
|
||||
|
||||
.shutdown-button.shutdown-disabled svg {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
|
||||
/* Loading line animation */
|
||||
.loading-line {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: #ccc;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.loading-line::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, transparent, #007bff, transparent);
|
||||
animation: loading 1.5s infinite;
|
||||
}
|
||||
@keyframes loading {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
/* Hide the loading line once data is loaded */
|
||||
.usd-value:not(.loading) .loading-line,
|
||||
.profit-loss:not(.loading) .loading-line {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.resolution-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #4B5563; /* gray-600 */
|
||||
font-size: 0.875rem; /* text-sm */
|
||||
font-weight: 500; /* font-medium */
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: all 0.2s;
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.resolution-button:hover {
|
||||
color: #1F2937; /* gray-800 */
|
||||
}
|
||||
|
||||
.resolution-button:focus {
|
||||
outline: 2px solid #3B82F6; /* blue-500 */
|
||||
}
|
||||
|
||||
.resolution-button.active {
|
||||
color: #3B82F6; /* blue-500 */
|
||||
outline: 2px solid #3B82F6; /* blue-500 */
|
||||
}
|
||||
|
||||
.dark .resolution-button {
|
||||
color: #9CA3AF; /* gray-400 */
|
||||
}
|
||||
|
||||
.dark .resolution-button:hover {
|
||||
color: #F3F4F6; /* gray-100 */
|
||||
}
|
||||
|
||||
.dark .resolution-button.active {
|
||||
color: #60A5FA; /* blue-400 */
|
||||
outline-color: #60A5FA; /* blue-400 */
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#toggle-volume.active {
|
||||
@apply bg-green-500 hover:bg-green-600 focus:ring-green-300;
|
||||
}
|
||||
#toggle-auto-refresh[data-enabled="true"] {
|
||||
@apply bg-green-500 hover:bg-green-600 focus:ring-green-300;
|
||||
}
|
||||
|
||||
|
||||
BIN
basicswap/static/images/coins/Bitcoin-Cash-20.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
basicswap/static/images/coins/Bitcoin-Cash.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
basicswap/static/images/coins/Decred-20.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
basicswap/static/images/coins/Decred.png
Normal file
|
After Width: | Height: | Size: 4.2 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/Litecoin%MWEB.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/Wownero-20.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
basicswap/static/images/coins/Wownero.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
basicswap/static/images/coins/Zano-20.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
basicswap/static/images/coins/Zano.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
@@ -1,70 +1,68 @@
|
||||
// Define a cache object to store selected option data
|
||||
const selectCache = {};
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// Function to update the cache with the selected option data for a given select element
|
||||
function updateSelectCache(select) {
|
||||
const selectedOption = select.options[select.selectedIndex];
|
||||
const image = selectedOption.getAttribute('data-image');
|
||||
const name = selectedOption.textContent.trim();
|
||||
selectCache[select.id] = { image, name };
|
||||
}
|
||||
const selectCache = {};
|
||||
|
||||
// Function to set the selected option and associated image and name for a given select element
|
||||
function updateSelectCache(select) {
|
||||
const selectedOption = select.options[select.selectedIndex];
|
||||
const image = selectedOption.getAttribute('data-image');
|
||||
const name = selectedOption.textContent.trim();
|
||||
selectCache[select.id] = { image, name };
|
||||
}
|
||||
|
||||
function setSelectData(select) {
|
||||
const selectedOption = select.options[select.selectedIndex];
|
||||
const image = selectedOption.getAttribute('data-image') || '';
|
||||
const name = selectedOption.textContent.trim();
|
||||
select.style.backgroundImage = image ? `url(${image}?${new Date().getTime()})` : '';
|
||||
|
||||
const selectImage = select.nextElementSibling.querySelector('.select-image');
|
||||
if (selectImage) {
|
||||
selectImage.src = image;
|
||||
}
|
||||
|
||||
function setSelectData(select) {
|
||||
const selectedOption = select.options[select.selectedIndex];
|
||||
const image = selectedOption.getAttribute('data-image') || '/static/images/other/coin.png'; // set a default image URL
|
||||
const name = selectedOption.textContent.trim();
|
||||
if (image) {
|
||||
select.style.backgroundImage = `url(${image})`;
|
||||
select.nextElementSibling.querySelector('.select-image').src = image;
|
||||
} else {
|
||||
select.style.backgroundImage = '';
|
||||
select.nextElementSibling.querySelector('.select-image').src = '';
|
||||
}
|
||||
select.nextElementSibling.querySelector('.select-name').textContent = name;
|
||||
updateSelectCache(select);
|
||||
}
|
||||
const selectNameElement = select.nextElementSibling.querySelector('.select-name');
|
||||
if (selectNameElement) {
|
||||
selectNameElement.textContent = name;
|
||||
}
|
||||
|
||||
updateSelectCache(select);
|
||||
}
|
||||
|
||||
// Function to get the selected option data from cache for a given select element
|
||||
function getSelectData(select) {
|
||||
return selectCache[select.id] || {};
|
||||
}
|
||||
const selectIcons = document.querySelectorAll('.custom-select .select-icon');
|
||||
const selectImages = document.querySelectorAll('.custom-select .select-image');
|
||||
const selectNames = document.querySelectorAll('.custom-select .select-name');
|
||||
|
||||
// Update all custom select elements on the page
|
||||
const selects = document.querySelectorAll('.custom-select .select');
|
||||
selects.forEach((select) => {
|
||||
// Set the initial select data based on the cached data (if available) or the selected option (if any)
|
||||
const cachedData = getSelectData(select);
|
||||
if (cachedData.image) {
|
||||
select.style.backgroundImage = `url(${cachedData.image})`;
|
||||
select.nextElementSibling.querySelector('.select-image').src = cachedData.image;
|
||||
}
|
||||
if (cachedData.name) {
|
||||
select.nextElementSibling.querySelector('.select-name').textContent = cachedData.name;
|
||||
}
|
||||
if (select.selectedIndex >= 0) {
|
||||
setSelectData(select);
|
||||
}
|
||||
selectIcons.forEach(icon => icon.style.display = 'none');
|
||||
selectImages.forEach(image => image.style.display = 'none');
|
||||
selectNames.forEach(name => name.style.display = 'none');
|
||||
|
||||
// Add event listener to update select data when an option is selected
|
||||
select.addEventListener('change', () => {
|
||||
setSelectData(select);
|
||||
});
|
||||
});
|
||||
function setupCustomSelect(select) {
|
||||
const options = select.querySelectorAll('option');
|
||||
const selectIcon = select.parentElement.querySelector('.select-icon');
|
||||
const selectImage = select.parentElement.querySelector('.select-image');
|
||||
|
||||
// Hide the select image and name on page load
|
||||
const selectIcons = document.querySelectorAll('.custom-select .select-icon');
|
||||
const selectImages = document.querySelectorAll('.custom-select .select-image');
|
||||
const selectNames = document.querySelectorAll('.custom-select .select-name');
|
||||
selectIcons.forEach((icon) => {
|
||||
icon.style.display = 'none';
|
||||
});
|
||||
selectImages.forEach((image) => {
|
||||
image.style.display = 'none';
|
||||
});
|
||||
selectNames.forEach((name) => {
|
||||
name.style.display = 'none';
|
||||
});
|
||||
options.forEach(option => {
|
||||
const image = option.getAttribute('data-image');
|
||||
if (image) {
|
||||
option.style.backgroundImage = `url(${image})`;
|
||||
}
|
||||
});
|
||||
|
||||
const storedValue = localStorage.getItem(select.name);
|
||||
if (storedValue && select.value == '-1') {
|
||||
select.value = storedValue;
|
||||
}
|
||||
|
||||
select.addEventListener('change', () => {
|
||||
setSelectData(select);
|
||||
localStorage.setItem(select.name, select.value);
|
||||
});
|
||||
|
||||
setSelectData(select);
|
||||
selectIcon.style.display = 'none';
|
||||
selectImage.style.display = 'none';
|
||||
}
|
||||
|
||||
const customSelects = document.querySelectorAll('.custom-select select');
|
||||
customSelects.forEach(setupCustomSelect);
|
||||
});
|
||||
@@ -1,60 +0,0 @@
|
||||
// Define the function for setting up the custom select element
|
||||
function setupCustomSelect(select) {
|
||||
const options = select.querySelectorAll('option');
|
||||
const selectIcon = select.parentElement.querySelector('.select-icon');
|
||||
const selectImage = select.parentElement.querySelector('.select-image');
|
||||
|
||||
// Set the background image for each option that has a data-image attribute
|
||||
options.forEach(option => {
|
||||
const image = option.getAttribute('data-image');
|
||||
if (image) {
|
||||
option.style.backgroundImage = `url(${image})`;
|
||||
}
|
||||
});
|
||||
|
||||
if (select.value == '-1') {
|
||||
// Set the selected option based on the stored value
|
||||
const storedValue = localStorage.getItem(select.name);
|
||||
if (storedValue) {
|
||||
select.value = storedValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Set the selected option image based on the selected value
|
||||
const selectedOption = select.querySelector(`option[value="${select.value}"]`);
|
||||
if (selectedOption) {
|
||||
const image = selectedOption.getAttribute('data-image');
|
||||
if (image) {
|
||||
select.style.backgroundImage = `url(${image})`;
|
||||
selectImage.src = image;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the select element and image when the user makes a selection
|
||||
select.addEventListener('change', () => {
|
||||
const selectedOption = select.options[select.selectedIndex];
|
||||
const image = selectedOption.getAttribute('data-image');
|
||||
if (image) {
|
||||
select.style.backgroundImage = `url(${image})`;
|
||||
selectImage.src = image;
|
||||
} else {
|
||||
select.style.backgroundImage = '';
|
||||
selectImage.src = '';
|
||||
}
|
||||
|
||||
// Save the selected value to localStorage
|
||||
localStorage.setItem(select.name, select.value);
|
||||
});
|
||||
|
||||
// Hide the select icon and image on page load
|
||||
selectIcon.style.display = 'none';
|
||||
selectImage.style.display = 'none';
|
||||
}
|
||||
|
||||
// Call the setupCustomSelect function for each custom select element
|
||||
const customSelects = document.querySelectorAll('.custom-select select');
|
||||
customSelects.forEach(select => {
|
||||
setupCustomSelect(select);
|
||||
});
|
||||
|
||||
|
||||
7
basicswap/static/js/libs/chartjs-adapter-date-fns.bundle.min.js
vendored
Normal file
614
basicswap/static/js/libs/qrcode.js
Normal file
@@ -0,0 +1,614 @@
|
||||
/**
|
||||
* @fileoverview
|
||||
* - Using the 'QRCode for Javascript library'
|
||||
* - Fixed dataset of 'QRCode for Javascript library' for support full-spec.
|
||||
* - this library has no dependencies.
|
||||
*
|
||||
* @author davidshimjs
|
||||
* @see <a href="http://www.d-project.com/" target="_blank">http://www.d-project.com/</a>
|
||||
* @see <a href="http://jeromeetienne.github.com/jquery-qrcode/" target="_blank">http://jeromeetienne.github.com/jquery-qrcode/</a>
|
||||
*/
|
||||
var QRCode;
|
||||
|
||||
(function () {
|
||||
//---------------------------------------------------------------------
|
||||
// QRCode for JavaScript
|
||||
//
|
||||
// Copyright (c) 2009 Kazuhiko Arase
|
||||
//
|
||||
// URL: http://www.d-project.com/
|
||||
//
|
||||
// Licensed under the MIT license:
|
||||
// http://www.opensource.org/licenses/mit-license.php
|
||||
//
|
||||
// The word "QR Code" is registered trademark of
|
||||
// DENSO WAVE INCORPORATED
|
||||
// http://www.denso-wave.com/qrcode/faqpatent-e.html
|
||||
//
|
||||
//---------------------------------------------------------------------
|
||||
function QR8bitByte(data) {
|
||||
this.mode = QRMode.MODE_8BIT_BYTE;
|
||||
this.data = data;
|
||||
this.parsedData = [];
|
||||
|
||||
// Added to support UTF-8 Characters
|
||||
for (var i = 0, l = this.data.length; i < l; i++) {
|
||||
var byteArray = [];
|
||||
var code = this.data.charCodeAt(i);
|
||||
|
||||
if (code > 0x10000) {
|
||||
byteArray[0] = 0xF0 | ((code & 0x1C0000) >>> 18);
|
||||
byteArray[1] = 0x80 | ((code & 0x3F000) >>> 12);
|
||||
byteArray[2] = 0x80 | ((code & 0xFC0) >>> 6);
|
||||
byteArray[3] = 0x80 | (code & 0x3F);
|
||||
} else if (code > 0x800) {
|
||||
byteArray[0] = 0xE0 | ((code & 0xF000) >>> 12);
|
||||
byteArray[1] = 0x80 | ((code & 0xFC0) >>> 6);
|
||||
byteArray[2] = 0x80 | (code & 0x3F);
|
||||
} else if (code > 0x80) {
|
||||
byteArray[0] = 0xC0 | ((code & 0x7C0) >>> 6);
|
||||
byteArray[1] = 0x80 | (code & 0x3F);
|
||||
} else {
|
||||
byteArray[0] = code;
|
||||
}
|
||||
|
||||
this.parsedData.push(byteArray);
|
||||
}
|
||||
|
||||
this.parsedData = Array.prototype.concat.apply([], this.parsedData);
|
||||
|
||||
if (this.parsedData.length != this.data.length) {
|
||||
this.parsedData.unshift(191);
|
||||
this.parsedData.unshift(187);
|
||||
this.parsedData.unshift(239);
|
||||
}
|
||||
}
|
||||
|
||||
QR8bitByte.prototype = {
|
||||
getLength: function (buffer) {
|
||||
return this.parsedData.length;
|
||||
},
|
||||
write: function (buffer) {
|
||||
for (var i = 0, l = this.parsedData.length; i < l; i++) {
|
||||
buffer.put(this.parsedData[i], 8);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function QRCodeModel(typeNumber, errorCorrectLevel) {
|
||||
this.typeNumber = typeNumber;
|
||||
this.errorCorrectLevel = errorCorrectLevel;
|
||||
this.modules = null;
|
||||
this.moduleCount = 0;
|
||||
this.dataCache = null;
|
||||
this.dataList = [];
|
||||
}
|
||||
|
||||
QRCodeModel.prototype={addData:function(data){var newData=new QR8bitByte(data);this.dataList.push(newData);this.dataCache=null;},isDark:function(row,col){if(row<0||this.moduleCount<=row||col<0||this.moduleCount<=col){throw new Error(row+","+col);}
|
||||
return this.modules[row][col];},getModuleCount:function(){return this.moduleCount;},make:function(){this.makeImpl(false,this.getBestMaskPattern());},makeImpl:function(test,maskPattern){this.moduleCount=this.typeNumber*4+17;this.modules=new Array(this.moduleCount);for(var row=0;row<this.moduleCount;row++){this.modules[row]=new Array(this.moduleCount);for(var col=0;col<this.moduleCount;col++){this.modules[row][col]=null;}}
|
||||
this.setupPositionProbePattern(0,0);this.setupPositionProbePattern(this.moduleCount-7,0);this.setupPositionProbePattern(0,this.moduleCount-7);this.setupPositionAdjustPattern();this.setupTimingPattern();this.setupTypeInfo(test,maskPattern);if(this.typeNumber>=7){this.setupTypeNumber(test);}
|
||||
if(this.dataCache==null){this.dataCache=QRCodeModel.createData(this.typeNumber,this.errorCorrectLevel,this.dataList);}
|
||||
this.mapData(this.dataCache,maskPattern);},setupPositionProbePattern:function(row,col){for(var r=-1;r<=7;r++){if(row+r<=-1||this.moduleCount<=row+r)continue;for(var c=-1;c<=7;c++){if(col+c<=-1||this.moduleCount<=col+c)continue;if((0<=r&&r<=6&&(c==0||c==6))||(0<=c&&c<=6&&(r==0||r==6))||(2<=r&&r<=4&&2<=c&&c<=4)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}},getBestMaskPattern:function(){var minLostPoint=0;var pattern=0;for(var i=0;i<8;i++){this.makeImpl(true,i);var lostPoint=QRUtil.getLostPoint(this);if(i==0||minLostPoint>lostPoint){minLostPoint=lostPoint;pattern=i;}}
|
||||
return pattern;},createMovieClip:function(target_mc,instance_name,depth){var qr_mc=target_mc.createEmptyMovieClip(instance_name,depth);var cs=1;this.make();for(var row=0;row<this.modules.length;row++){var y=row*cs;for(var col=0;col<this.modules[row].length;col++){var x=col*cs;var dark=this.modules[row][col];if(dark){qr_mc.beginFill(0,100);qr_mc.moveTo(x,y);qr_mc.lineTo(x+cs,y);qr_mc.lineTo(x+cs,y+cs);qr_mc.lineTo(x,y+cs);qr_mc.endFill();}}}
|
||||
return qr_mc;},setupTimingPattern:function(){for(var r=8;r<this.moduleCount-8;r++){if(this.modules[r][6]!=null){continue;}
|
||||
this.modules[r][6]=(r%2==0);}
|
||||
for(var c=8;c<this.moduleCount-8;c++){if(this.modules[6][c]!=null){continue;}
|
||||
this.modules[6][c]=(c%2==0);}},setupPositionAdjustPattern:function(){var pos=QRUtil.getPatternPosition(this.typeNumber);for(var i=0;i<pos.length;i++){for(var j=0;j<pos.length;j++){var row=pos[i];var col=pos[j];if(this.modules[row][col]!=null){continue;}
|
||||
for(var r=-2;r<=2;r++){for(var c=-2;c<=2;c++){if(r==-2||r==2||c==-2||c==2||(r==0&&c==0)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}}}},setupTypeNumber:function(test){var bits=QRUtil.getBCHTypeNumber(this.typeNumber);for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[Math.floor(i/3)][i%3+this.moduleCount-8-3]=mod;}
|
||||
for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[i%3+this.moduleCount-8-3][Math.floor(i/3)]=mod;}},setupTypeInfo:function(test,maskPattern){var data=(this.errorCorrectLevel<<3)|maskPattern;var bits=QRUtil.getBCHTypeInfo(data);for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<6){this.modules[i][8]=mod;}else if(i<8){this.modules[i+1][8]=mod;}else{this.modules[this.moduleCount-15+i][8]=mod;}}
|
||||
for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<8){this.modules[8][this.moduleCount-i-1]=mod;}else if(i<9){this.modules[8][15-i-1+1]=mod;}else{this.modules[8][15-i-1]=mod;}}
|
||||
this.modules[this.moduleCount-8][8]=(!test);},mapData:function(data,maskPattern){var inc=-1;var row=this.moduleCount-1;var bitIndex=7;var byteIndex=0;for(var col=this.moduleCount-1;col>0;col-=2){if(col==6)col--;while(true){for(var c=0;c<2;c++){if(this.modules[row][col-c]==null){var dark=false;if(byteIndex<data.length){dark=(((data[byteIndex]>>>bitIndex)&1)==1);}
|
||||
var mask=QRUtil.getMask(maskPattern,row,col-c);if(mask){dark=!dark;}
|
||||
this.modules[row][col-c]=dark;bitIndex--;if(bitIndex==-1){byteIndex++;bitIndex=7;}}}
|
||||
row+=inc;if(row<0||this.moduleCount<=row){row-=inc;inc=-inc;break;}}}}};QRCodeModel.PAD0=0xEC;QRCodeModel.PAD1=0x11;QRCodeModel.createData=function(typeNumber,errorCorrectLevel,dataList){var rsBlocks=QRRSBlock.getRSBlocks(typeNumber,errorCorrectLevel);var buffer=new QRBitBuffer();for(var i=0;i<dataList.length;i++){var data=dataList[i];buffer.put(data.mode,4);buffer.put(data.getLength(),QRUtil.getLengthInBits(data.mode,typeNumber));data.write(buffer);}
|
||||
var totalDataCount=0;for(var i=0;i<rsBlocks.length;i++){totalDataCount+=rsBlocks[i].dataCount;}
|
||||
if(buffer.getLengthInBits()>totalDataCount*8){throw new Error("code length overflow. ("
|
||||
+buffer.getLengthInBits()
|
||||
+">"
|
||||
+totalDataCount*8
|
||||
+")");}
|
||||
if(buffer.getLengthInBits()+4<=totalDataCount*8){buffer.put(0,4);}
|
||||
while(buffer.getLengthInBits()%8!=0){buffer.putBit(false);}
|
||||
while(true){if(buffer.getLengthInBits()>=totalDataCount*8){break;}
|
||||
buffer.put(QRCodeModel.PAD0,8);if(buffer.getLengthInBits()>=totalDataCount*8){break;}
|
||||
buffer.put(QRCodeModel.PAD1,8);}
|
||||
return QRCodeModel.createBytes(buffer,rsBlocks);};QRCodeModel.createBytes=function(buffer,rsBlocks){var offset=0;var maxDcCount=0;var maxEcCount=0;var dcdata=new Array(rsBlocks.length);var ecdata=new Array(rsBlocks.length);for(var r=0;r<rsBlocks.length;r++){var dcCount=rsBlocks[r].dataCount;var ecCount=rsBlocks[r].totalCount-dcCount;maxDcCount=Math.max(maxDcCount,dcCount);maxEcCount=Math.max(maxEcCount,ecCount);dcdata[r]=new Array(dcCount);for(var i=0;i<dcdata[r].length;i++){dcdata[r][i]=0xff&buffer.buffer[i+offset];}
|
||||
offset+=dcCount;var rsPoly=QRUtil.getErrorCorrectPolynomial(ecCount);var rawPoly=new QRPolynomial(dcdata[r],rsPoly.getLength()-1);var modPoly=rawPoly.mod(rsPoly);ecdata[r]=new Array(rsPoly.getLength()-1);for(var i=0;i<ecdata[r].length;i++){var modIndex=i+modPoly.getLength()-ecdata[r].length;ecdata[r][i]=(modIndex>=0)?modPoly.get(modIndex):0;}}
|
||||
var totalCodeCount=0;for(var i=0;i<rsBlocks.length;i++){totalCodeCount+=rsBlocks[i].totalCount;}
|
||||
var data=new Array(totalCodeCount);var index=0;for(var i=0;i<maxDcCount;i++){for(var r=0;r<rsBlocks.length;r++){if(i<dcdata[r].length){data[index++]=dcdata[r][i];}}}
|
||||
for(var i=0;i<maxEcCount;i++){for(var r=0;r<rsBlocks.length;r++){if(i<ecdata[r].length){data[index++]=ecdata[r][i];}}}
|
||||
return data;};var QRMode={MODE_NUMBER:1<<0,MODE_ALPHA_NUM:1<<1,MODE_8BIT_BYTE:1<<2,MODE_KANJI:1<<3};var QRErrorCorrectLevel={L:1,M:0,Q:3,H:2};var QRMaskPattern={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7};var QRUtil={PATTERN_POSITION_TABLE:[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],G15:(1<<10)|(1<<8)|(1<<5)|(1<<4)|(1<<2)|(1<<1)|(1<<0),G18:(1<<12)|(1<<11)|(1<<10)|(1<<9)|(1<<8)|(1<<5)|(1<<2)|(1<<0),G15_MASK:(1<<14)|(1<<12)|(1<<10)|(1<<4)|(1<<1),getBCHTypeInfo:function(data){var d=data<<10;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)>=0){d^=(QRUtil.G15<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)));}
|
||||
return((data<<10)|d)^QRUtil.G15_MASK;},getBCHTypeNumber:function(data){var d=data<<12;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)>=0){d^=(QRUtil.G18<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)));}
|
||||
return(data<<12)|d;},getBCHDigit:function(data){var digit=0;while(data!=0){digit++;data>>>=1;}
|
||||
return digit;},getPatternPosition:function(typeNumber){return QRUtil.PATTERN_POSITION_TABLE[typeNumber-1];},getMask:function(maskPattern,i,j){switch(maskPattern){case QRMaskPattern.PATTERN000:return(i+j)%2==0;case QRMaskPattern.PATTERN001:return i%2==0;case QRMaskPattern.PATTERN010:return j%3==0;case QRMaskPattern.PATTERN011:return(i+j)%3==0;case QRMaskPattern.PATTERN100:return(Math.floor(i/2)+Math.floor(j/3))%2==0;case QRMaskPattern.PATTERN101:return(i*j)%2+(i*j)%3==0;case QRMaskPattern.PATTERN110:return((i*j)%2+(i*j)%3)%2==0;case QRMaskPattern.PATTERN111:return((i*j)%3+(i+j)%2)%2==0;default:throw new Error("bad maskPattern:"+maskPattern);}},getErrorCorrectPolynomial:function(errorCorrectLength){var a=new QRPolynomial([1],0);for(var i=0;i<errorCorrectLength;i++){a=a.multiply(new QRPolynomial([1,QRMath.gexp(i)],0));}
|
||||
return a;},getLengthInBits:function(mode,type){if(1<=type&&type<10){switch(mode){case QRMode.MODE_NUMBER:return 10;case QRMode.MODE_ALPHA_NUM:return 9;case QRMode.MODE_8BIT_BYTE:return 8;case QRMode.MODE_KANJI:return 8;default:throw new Error("mode:"+mode);}}else if(type<27){switch(mode){case QRMode.MODE_NUMBER:return 12;case QRMode.MODE_ALPHA_NUM:return 11;case QRMode.MODE_8BIT_BYTE:return 16;case QRMode.MODE_KANJI:return 10;default:throw new Error("mode:"+mode);}}else if(type<41){switch(mode){case QRMode.MODE_NUMBER:return 14;case QRMode.MODE_ALPHA_NUM:return 13;case QRMode.MODE_8BIT_BYTE:return 16;case QRMode.MODE_KANJI:return 12;default:throw new Error("mode:"+mode);}}else{throw new Error("type:"+type);}},getLostPoint:function(qrCode){var moduleCount=qrCode.getModuleCount();var lostPoint=0;for(var row=0;row<moduleCount;row++){for(var col=0;col<moduleCount;col++){var sameCount=0;var dark=qrCode.isDark(row,col);for(var r=-1;r<=1;r++){if(row+r<0||moduleCount<=row+r){continue;}
|
||||
for(var c=-1;c<=1;c++){if(col+c<0||moduleCount<=col+c){continue;}
|
||||
if(r==0&&c==0){continue;}
|
||||
if(dark==qrCode.isDark(row+r,col+c)){sameCount++;}}}
|
||||
if(sameCount>5){lostPoint+=(3+sameCount-5);}}}
|
||||
for(var row=0;row<moduleCount-1;row++){for(var col=0;col<moduleCount-1;col++){var count=0;if(qrCode.isDark(row,col))count++;if(qrCode.isDark(row+1,col))count++;if(qrCode.isDark(row,col+1))count++;if(qrCode.isDark(row+1,col+1))count++;if(count==0||count==4){lostPoint+=3;}}}
|
||||
for(var row=0;row<moduleCount;row++){for(var col=0;col<moduleCount-6;col++){if(qrCode.isDark(row,col)&&!qrCode.isDark(row,col+1)&&qrCode.isDark(row,col+2)&&qrCode.isDark(row,col+3)&&qrCode.isDark(row,col+4)&&!qrCode.isDark(row,col+5)&&qrCode.isDark(row,col+6)){lostPoint+=40;}}}
|
||||
for(var col=0;col<moduleCount;col++){for(var row=0;row<moduleCount-6;row++){if(qrCode.isDark(row,col)&&!qrCode.isDark(row+1,col)&&qrCode.isDark(row+2,col)&&qrCode.isDark(row+3,col)&&qrCode.isDark(row+4,col)&&!qrCode.isDark(row+5,col)&&qrCode.isDark(row+6,col)){lostPoint+=40;}}}
|
||||
var darkCount=0;for(var col=0;col<moduleCount;col++){for(var row=0;row<moduleCount;row++){if(qrCode.isDark(row,col)){darkCount++;}}}
|
||||
var ratio=Math.abs(100*darkCount/moduleCount/moduleCount-50)/5;lostPoint+=ratio*10;return lostPoint;}};var QRMath={glog:function(n){if(n<1){throw new Error("glog("+n+")");}
|
||||
return QRMath.LOG_TABLE[n];},gexp:function(n){while(n<0){n+=255;}
|
||||
while(n>=256){n-=255;}
|
||||
return QRMath.EXP_TABLE[n];},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)};for(var i=0;i<8;i++){QRMath.EXP_TABLE[i]=1<<i;}
|
||||
for(var i=8;i<256;i++){QRMath.EXP_TABLE[i]=QRMath.EXP_TABLE[i-4]^QRMath.EXP_TABLE[i-5]^QRMath.EXP_TABLE[i-6]^QRMath.EXP_TABLE[i-8];}
|
||||
for(var i=0;i<255;i++){QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]]=i;}
|
||||
function QRPolynomial(num,shift){if(num.length==undefined){throw new Error(num.length+"/"+shift);}
|
||||
var offset=0;while(offset<num.length&&num[offset]==0){offset++;}
|
||||
this.num=new Array(num.length-offset+shift);for(var i=0;i<num.length-offset;i++){this.num[i]=num[i+offset];}}
|
||||
QRPolynomial.prototype={get:function(index){return this.num[index];},getLength:function(){return this.num.length;},multiply:function(e){var num=new Array(this.getLength()+e.getLength()-1);for(var i=0;i<this.getLength();i++){for(var j=0;j<e.getLength();j++){num[i+j]^=QRMath.gexp(QRMath.glog(this.get(i))+QRMath.glog(e.get(j)));}}
|
||||
return new QRPolynomial(num,0);},mod:function(e){if(this.getLength()-e.getLength()<0){return this;}
|
||||
var ratio=QRMath.glog(this.get(0))-QRMath.glog(e.get(0));var num=new Array(this.getLength());for(var i=0;i<this.getLength();i++){num[i]=this.get(i);}
|
||||
for(var i=0;i<e.getLength();i++){num[i]^=QRMath.gexp(QRMath.glog(e.get(i))+ratio);}
|
||||
return new QRPolynomial(num,0).mod(e);}};function QRRSBlock(totalCount,dataCount){this.totalCount=totalCount;this.dataCount=dataCount;}
|
||||
QRRSBlock.RS_BLOCK_TABLE=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]];QRRSBlock.getRSBlocks=function(typeNumber,errorCorrectLevel){var rsBlock=QRRSBlock.getRsBlockTable(typeNumber,errorCorrectLevel);if(rsBlock==undefined){throw new Error("bad rs block @ typeNumber:"+typeNumber+"/errorCorrectLevel:"+errorCorrectLevel);}
|
||||
var length=rsBlock.length/3;var list=[];for(var i=0;i<length;i++){var count=rsBlock[i*3+0];var totalCount=rsBlock[i*3+1];var dataCount=rsBlock[i*3+2];for(var j=0;j<count;j++){list.push(new QRRSBlock(totalCount,dataCount));}}
|
||||
return list;};QRRSBlock.getRsBlockTable=function(typeNumber,errorCorrectLevel){switch(errorCorrectLevel){case QRErrorCorrectLevel.L:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+0];case QRErrorCorrectLevel.M:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+1];case QRErrorCorrectLevel.Q:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+2];case QRErrorCorrectLevel.H:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+3];default:return undefined;}};function QRBitBuffer(){this.buffer=[];this.length=0;}
|
||||
QRBitBuffer.prototype={get:function(index){var bufIndex=Math.floor(index/8);return((this.buffer[bufIndex]>>>(7-index%8))&1)==1;},put:function(num,length){for(var i=0;i<length;i++){this.putBit(((num>>>(length-i-1))&1)==1);}},getLengthInBits:function(){return this.length;},putBit:function(bit){var bufIndex=Math.floor(this.length/8);if(this.buffer.length<=bufIndex){this.buffer.push(0);}
|
||||
if(bit){this.buffer[bufIndex]|=(0x80>>>(this.length%8));}
|
||||
this.length++;}};var QRCodeLimitLength=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]];
|
||||
|
||||
function _isSupportCanvas() {
|
||||
return typeof CanvasRenderingContext2D != "undefined";
|
||||
}
|
||||
|
||||
// android 2.x doesn't support Data-URI spec
|
||||
function _getAndroid() {
|
||||
var android = false;
|
||||
var sAgent = navigator.userAgent;
|
||||
|
||||
if (/android/i.test(sAgent)) { // android
|
||||
android = true;
|
||||
var aMat = sAgent.toString().match(/android ([0-9]\.[0-9])/i);
|
||||
|
||||
if (aMat && aMat[1]) {
|
||||
android = parseFloat(aMat[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return android;
|
||||
}
|
||||
|
||||
var svgDrawer = (function() {
|
||||
|
||||
var Drawing = function (el, htOption) {
|
||||
this._el = el;
|
||||
this._htOption = htOption;
|
||||
};
|
||||
|
||||
Drawing.prototype.draw = function (oQRCode) {
|
||||
var _htOption = this._htOption;
|
||||
var _el = this._el;
|
||||
var nCount = oQRCode.getModuleCount();
|
||||
var nWidth = Math.floor(_htOption.width / nCount);
|
||||
var nHeight = Math.floor(_htOption.height / nCount);
|
||||
|
||||
this.clear();
|
||||
|
||||
function makeSVG(tag, attrs) {
|
||||
var el = document.createElementNS('http://www.w3.org/2000/svg', tag);
|
||||
for (var k in attrs)
|
||||
if (attrs.hasOwnProperty(k)) el.setAttribute(k, attrs[k]);
|
||||
return el;
|
||||
}
|
||||
|
||||
var svg = makeSVG("svg" , {'viewBox': '0 0 ' + String(nCount) + " " + String(nCount), 'width': '100%', 'height': '100%', 'fill': _htOption.colorLight});
|
||||
svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink");
|
||||
_el.appendChild(svg);
|
||||
|
||||
svg.appendChild(makeSVG("rect", {"fill": _htOption.colorLight, "width": "100%", "height": "100%"}));
|
||||
svg.appendChild(makeSVG("rect", {"fill": _htOption.colorDark, "width": "1", "height": "1", "id": "template"}));
|
||||
|
||||
for (var row = 0; row < nCount; row++) {
|
||||
for (var col = 0; col < nCount; col++) {
|
||||
if (oQRCode.isDark(row, col)) {
|
||||
var child = makeSVG("use", {"x": String(row), "y": String(col)});
|
||||
child.setAttributeNS("http://www.w3.org/1999/xlink", "href", "#template")
|
||||
svg.appendChild(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Drawing.prototype.clear = function () {
|
||||
while (this._el.hasChildNodes())
|
||||
this._el.removeChild(this._el.lastChild);
|
||||
};
|
||||
return Drawing;
|
||||
})();
|
||||
|
||||
var useSVG = document.documentElement.tagName.toLowerCase() === "svg";
|
||||
|
||||
// Drawing in DOM by using Table tag
|
||||
var Drawing = useSVG ? svgDrawer : !_isSupportCanvas() ? (function () {
|
||||
var Drawing = function (el, htOption) {
|
||||
this._el = el;
|
||||
this._htOption = htOption;
|
||||
};
|
||||
|
||||
/**
|
||||
* Draw the QRCode
|
||||
*
|
||||
* @param {QRCode} oQRCode
|
||||
*/
|
||||
Drawing.prototype.draw = function (oQRCode) {
|
||||
var _htOption = this._htOption;
|
||||
var _el = this._el;
|
||||
var nCount = oQRCode.getModuleCount();
|
||||
var nWidth = Math.floor(_htOption.width / nCount);
|
||||
var nHeight = Math.floor(_htOption.height / nCount);
|
||||
var aHTML = ['<table style="border:0;border-collapse:collapse;">'];
|
||||
|
||||
for (var row = 0; row < nCount; row++) {
|
||||
aHTML.push('<tr>');
|
||||
|
||||
for (var col = 0; col < nCount; col++) {
|
||||
aHTML.push('<td style="border:0;border-collapse:collapse;padding:0;margin:0;width:' + nWidth + 'px;height:' + nHeight + 'px;background-color:' + (oQRCode.isDark(row, col) ? _htOption.colorDark : _htOption.colorLight) + ';"></td>');
|
||||
}
|
||||
|
||||
aHTML.push('</tr>');
|
||||
}
|
||||
|
||||
aHTML.push('</table>');
|
||||
_el.innerHTML = aHTML.join('');
|
||||
|
||||
// Fix the margin values as real size.
|
||||
var elTable = _el.childNodes[0];
|
||||
var nLeftMarginTable = (_htOption.width - elTable.offsetWidth) / 2;
|
||||
var nTopMarginTable = (_htOption.height - elTable.offsetHeight) / 2;
|
||||
|
||||
if (nLeftMarginTable > 0 && nTopMarginTable > 0) {
|
||||
elTable.style.margin = nTopMarginTable + "px " + nLeftMarginTable + "px";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear the QRCode
|
||||
*/
|
||||
Drawing.prototype.clear = function () {
|
||||
this._el.innerHTML = '';
|
||||
};
|
||||
|
||||
return Drawing;
|
||||
})() : (function () { // Drawing in Canvas
|
||||
function _onMakeImage() {
|
||||
this._elImage.src = this._elCanvas.toDataURL("image/png");
|
||||
this._elImage.style.display = "block";
|
||||
this._elCanvas.style.display = "none";
|
||||
}
|
||||
|
||||
// Android 2.1 bug workaround
|
||||
// http://code.google.com/p/android/issues/detail?id=5141
|
||||
if (this._android && this._android <= 2.1) {
|
||||
var factor = 1 / window.devicePixelRatio;
|
||||
var drawImage = CanvasRenderingContext2D.prototype.drawImage;
|
||||
CanvasRenderingContext2D.prototype.drawImage = function (image, sx, sy, sw, sh, dx, dy, dw, dh) {
|
||||
if (("nodeName" in image) && /img/i.test(image.nodeName)) {
|
||||
for (var i = arguments.length - 1; i >= 1; i--) {
|
||||
arguments[i] = arguments[i] * factor;
|
||||
}
|
||||
} else if (typeof dw == "undefined") {
|
||||
arguments[1] *= factor;
|
||||
arguments[2] *= factor;
|
||||
arguments[3] *= factor;
|
||||
arguments[4] *= factor;
|
||||
}
|
||||
|
||||
drawImage.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the user's browser supports Data URI or not
|
||||
*
|
||||
* @private
|
||||
* @param {Function} fSuccess Occurs if it supports Data URI
|
||||
* @param {Function} fFail Occurs if it doesn't support Data URI
|
||||
*/
|
||||
function _safeSetDataURI(fSuccess, fFail) {
|
||||
var self = this;
|
||||
self._fFail = fFail;
|
||||
self._fSuccess = fSuccess;
|
||||
|
||||
// Check it just once
|
||||
if (self._bSupportDataURI === null) {
|
||||
var el = document.createElement("img");
|
||||
var fOnError = function() {
|
||||
self._bSupportDataURI = false;
|
||||
|
||||
if (self._fFail) {
|
||||
self._fFail.call(self);
|
||||
}
|
||||
};
|
||||
var fOnSuccess = function() {
|
||||
self._bSupportDataURI = true;
|
||||
|
||||
if (self._fSuccess) {
|
||||
self._fSuccess.call(self);
|
||||
}
|
||||
};
|
||||
|
||||
el.onabort = fOnError;
|
||||
el.onerror = fOnError;
|
||||
el.onload = fOnSuccess;
|
||||
el.src = "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; // the Image contains 1px data.
|
||||
return;
|
||||
} else if (self._bSupportDataURI === true && self._fSuccess) {
|
||||
self._fSuccess.call(self);
|
||||
} else if (self._bSupportDataURI === false && self._fFail) {
|
||||
self._fFail.call(self);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Drawing QRCode by using canvas
|
||||
*
|
||||
* @constructor
|
||||
* @param {HTMLElement} el
|
||||
* @param {Object} htOption QRCode Options
|
||||
*/
|
||||
var Drawing = function (el, htOption) {
|
||||
this._bIsPainted = false;
|
||||
this._android = _getAndroid();
|
||||
|
||||
this._htOption = htOption;
|
||||
this._elCanvas = document.createElement("canvas");
|
||||
this._elCanvas.width = htOption.width;
|
||||
this._elCanvas.height = htOption.height;
|
||||
el.appendChild(this._elCanvas);
|
||||
this._el = el;
|
||||
this._oContext = this._elCanvas.getContext("2d");
|
||||
this._bIsPainted = false;
|
||||
this._elImage = document.createElement("img");
|
||||
this._elImage.alt = "Scan me!";
|
||||
this._elImage.style.display = "none";
|
||||
this._el.appendChild(this._elImage);
|
||||
this._bSupportDataURI = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Draw the QRCode
|
||||
*
|
||||
* @param {QRCode} oQRCode
|
||||
*/
|
||||
Drawing.prototype.draw = function (oQRCode) {
|
||||
var _elImage = this._elImage;
|
||||
var _oContext = this._oContext;
|
||||
var _htOption = this._htOption;
|
||||
|
||||
var nCount = oQRCode.getModuleCount();
|
||||
var nWidth = _htOption.width / nCount;
|
||||
var nHeight = _htOption.height / nCount;
|
||||
var nRoundedWidth = Math.round(nWidth);
|
||||
var nRoundedHeight = Math.round(nHeight);
|
||||
|
||||
_elImage.style.display = "none";
|
||||
this.clear();
|
||||
|
||||
for (var row = 0; row < nCount; row++) {
|
||||
for (var col = 0; col < nCount; col++) {
|
||||
var bIsDark = oQRCode.isDark(row, col);
|
||||
var nLeft = col * nWidth;
|
||||
var nTop = row * nHeight;
|
||||
_oContext.strokeStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight;
|
||||
_oContext.lineWidth = 1;
|
||||
_oContext.fillStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight;
|
||||
_oContext.fillRect(nLeft, nTop, nWidth, nHeight);
|
||||
|
||||
// 안티 앨리어싱 방지 처리
|
||||
_oContext.strokeRect(
|
||||
Math.floor(nLeft) + 0.5,
|
||||
Math.floor(nTop) + 0.5,
|
||||
nRoundedWidth,
|
||||
nRoundedHeight
|
||||
);
|
||||
|
||||
_oContext.strokeRect(
|
||||
Math.ceil(nLeft) - 0.5,
|
||||
Math.ceil(nTop) - 0.5,
|
||||
nRoundedWidth,
|
||||
nRoundedHeight
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this._bIsPainted = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Make the image from Canvas if the browser supports Data URI.
|
||||
*/
|
||||
Drawing.prototype.makeImage = function () {
|
||||
if (this._bIsPainted) {
|
||||
_safeSetDataURI.call(this, _onMakeImage);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return whether the QRCode is painted or not
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
Drawing.prototype.isPainted = function () {
|
||||
return this._bIsPainted;
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear the QRCode
|
||||
*/
|
||||
Drawing.prototype.clear = function () {
|
||||
this._oContext.clearRect(0, 0, this._elCanvas.width, this._elCanvas.height);
|
||||
this._bIsPainted = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Number} nNumber
|
||||
*/
|
||||
Drawing.prototype.round = function (nNumber) {
|
||||
if (!nNumber) {
|
||||
return nNumber;
|
||||
}
|
||||
|
||||
return Math.floor(nNumber * 1000) / 1000;
|
||||
};
|
||||
|
||||
return Drawing;
|
||||
})();
|
||||
|
||||
/**
|
||||
* Get the type by string length
|
||||
*
|
||||
* @private
|
||||
* @param {String} sText
|
||||
* @param {Number} nCorrectLevel
|
||||
* @return {Number} type
|
||||
*/
|
||||
function _getTypeNumber(sText, nCorrectLevel) {
|
||||
var nType = 1;
|
||||
var length = _getUTF8Length(sText);
|
||||
|
||||
for (var i = 0, len = QRCodeLimitLength.length; i <= len; i++) {
|
||||
var nLimit = 0;
|
||||
|
||||
switch (nCorrectLevel) {
|
||||
case QRErrorCorrectLevel.L :
|
||||
nLimit = QRCodeLimitLength[i][0];
|
||||
break;
|
||||
case QRErrorCorrectLevel.M :
|
||||
nLimit = QRCodeLimitLength[i][1];
|
||||
break;
|
||||
case QRErrorCorrectLevel.Q :
|
||||
nLimit = QRCodeLimitLength[i][2];
|
||||
break;
|
||||
case QRErrorCorrectLevel.H :
|
||||
nLimit = QRCodeLimitLength[i][3];
|
||||
break;
|
||||
}
|
||||
|
||||
if (length <= nLimit) {
|
||||
break;
|
||||
} else {
|
||||
nType++;
|
||||
}
|
||||
}
|
||||
|
||||
if (nType > QRCodeLimitLength.length) {
|
||||
throw new Error("Too long data");
|
||||
}
|
||||
|
||||
return nType;
|
||||
}
|
||||
|
||||
function _getUTF8Length(sText) {
|
||||
var replacedText = encodeURI(sText).toString().replace(/\%[0-9a-fA-F]{2}/g, 'a');
|
||||
return replacedText.length + (replacedText.length != sText ? 3 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @class QRCode
|
||||
* @constructor
|
||||
* @example
|
||||
* new QRCode(document.getElementById("test"), "http://jindo.dev.naver.com/collie");
|
||||
*
|
||||
* @example
|
||||
* var oQRCode = new QRCode("test", {
|
||||
* text : "http://naver.com",
|
||||
* width : 128,
|
||||
* height : 128
|
||||
* });
|
||||
*
|
||||
* oQRCode.clear(); // Clear the QRCode.
|
||||
* oQRCode.makeCode("http://map.naver.com"); // Re-create the QRCode.
|
||||
*
|
||||
* @param {HTMLElement|String} el target element or 'id' attribute of element.
|
||||
* @param {Object|String} vOption
|
||||
* @param {String} vOption.text QRCode link data
|
||||
* @param {Number} [vOption.width=256]
|
||||
* @param {Number} [vOption.height=256]
|
||||
* @param {String} [vOption.colorDark="#000000"]
|
||||
* @param {String} [vOption.colorLight="#ffffff"]
|
||||
* @param {QRCode.CorrectLevel} [vOption.correctLevel=QRCode.CorrectLevel.H] [L|M|Q|H]
|
||||
*/
|
||||
QRCode = function (el, vOption) {
|
||||
this._htOption = {
|
||||
width : 256,
|
||||
height : 256,
|
||||
typeNumber : 4,
|
||||
colorDark : "#000000",
|
||||
colorLight : "#ffffff",
|
||||
correctLevel : QRErrorCorrectLevel.H
|
||||
};
|
||||
|
||||
if (typeof vOption === 'string') {
|
||||
vOption = {
|
||||
text : vOption
|
||||
};
|
||||
}
|
||||
|
||||
// Overwrites options
|
||||
if (vOption) {
|
||||
for (var i in vOption) {
|
||||
this._htOption[i] = vOption[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof el == "string") {
|
||||
el = document.getElementById(el);
|
||||
}
|
||||
|
||||
if (this._htOption.useSVG) {
|
||||
Drawing = svgDrawer;
|
||||
}
|
||||
|
||||
this._android = _getAndroid();
|
||||
this._el = el;
|
||||
this._oQRCode = null;
|
||||
this._oDrawing = new Drawing(this._el, this._htOption);
|
||||
|
||||
if (this._htOption.text) {
|
||||
this.makeCode(this._htOption.text);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Make the QRCode
|
||||
*
|
||||
* @param {String} sText link data
|
||||
*/
|
||||
QRCode.prototype.makeCode = function (sText) {
|
||||
this._oQRCode = new QRCodeModel(_getTypeNumber(sText, this._htOption.correctLevel), this._htOption.correctLevel);
|
||||
this._oQRCode.addData(sText);
|
||||
this._oQRCode.make();
|
||||
this._el.title = sText;
|
||||
this._oDrawing.draw(this._oQRCode);
|
||||
this.makeImage();
|
||||
};
|
||||
|
||||
/**
|
||||
* Make the Image from Canvas element
|
||||
* - It occurs automatically
|
||||
* - Android below 3 doesn't support Data-URI spec.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
QRCode.prototype.makeImage = function () {
|
||||
if (typeof this._oDrawing.makeImage == "function" && (!this._android || this._android >= 3)) {
|
||||
this._oDrawing.makeImage();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear the QRCode
|
||||
*/
|
||||
QRCode.prototype.clear = function () {
|
||||
this._oDrawing.clear();
|
||||
};
|
||||
|
||||
/**
|
||||
* @name QRCode.CorrectLevel
|
||||
*/
|
||||
QRCode.CorrectLevel = QRErrorCorrectLevel;
|
||||
})();
|
||||
@@ -37,4 +37,23 @@ window.addEventListener('DOMContentLoaded', (event) => {
|
||||
event.target.classList.remove('error');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const selects = document.querySelectorAll('select.disabled-select');
|
||||
for (const select of selects) {
|
||||
if (select.disabled) {
|
||||
select.classList.add('disabled-select-enabled');
|
||||
} else {
|
||||
select.classList.remove('disabled-select-enabled');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const inputs = document.querySelectorAll('input.disabled-input, input[type="checkbox"].disabled-input');
|
||||
for (const input of inputs) {
|
||||
if (input.readOnly) {
|
||||
input.classList.add('disabled-input-enabled');
|
||||
} else {
|
||||
input.classList.remove('disabled-input-enabled');
|
||||
}
|
||||
}
|
||||
|
||||
1898
basicswap/static/js/offerstable.js
Normal file
1388
basicswap/static/js/pricechart.js
Normal file
@@ -1,4 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="mscgenjsreplaceme" width="1272" height="2063.25" class="mscgenjsreplaceme" style="font-family:Helvetica,sans-serif;font-size:12px;font-weight:400;font-style:normal;text-decoration:none;background-color:#fff;stroke:#000;stroke-width:2" version="1.1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="mscgenjsreplaceme" width="1272" height="2101.25" class="mscgenjsreplaceme" style="font-family:Helvetica,sans-serif;font-size:12px;font-weight:400;font-style:normal;text-decoration:none;background-color:#fff;stroke:#000;stroke-width:2" version="1.1">
|
||||
<defs>
|
||||
<marker id="mscgenjsreplacemecallback-#0000FF" class="arrow-marker" markerHeight="10" markerUnits="strokeWidth" markerWidth="10" orient="auto" refX="9" refY="3" viewBox="0 0 10 10">
|
||||
<path d="m1 1 8 2-8 2" class="arrow-style" style="stroke-dasharray:100,1;stroke:#00f"/>
|
||||
@@ -35,10 +35,10 @@
|
||||
</style>
|
||||
</defs>
|
||||
<g id="mscgenjsreplaceme_body" transform="translate(51 3)">
|
||||
<path id="mscgenjsreplaceme_background" d="M-51-3h1272v2063.25H-51z" class="bglayer" style="fill:#fff;stroke:#fff;stroke-width:0"/>
|
||||
<path id="mscgenjsreplaceme_background" d="M-51-3h1272v2101.25H-51z" class="bglayer" style="fill:#fff;stroke:#fff;stroke-width:0"/>
|
||||
<g id="mscgenjsreplaceme_arcspans">
|
||||
<path d="M-41 869.1h1044v1169.15H-41z" class="box inline_expression alt"/>
|
||||
<path d="M-37 1410.15H999v590.1H-37z" class="box inline_expression alt"/>
|
||||
<path d="M-41 907.1h1044v1169.15H-41z" class="box inline_expression alt"/>
|
||||
<path d="M-37 1448.15H999v590.1H-37z" class="box inline_expression alt"/>
|
||||
</g>
|
||||
<g id="mscgenjsreplaceme_lifelines">
|
||||
<path d="M65 38v38" class="arcrow" style="stroke:transparent"/>
|
||||
@@ -97,39 +97,39 @@
|
||||
<path d="M273 548v38" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 548v38" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 548v38" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 548v38M1105 548v38M65 586v75.05" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 586v75.05" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 586v75.05" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 586v75.05" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 586v75.05M1105 586v75.05M65 661.05v38" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 661.05v38" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 661.05v38" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 661.05v38" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 661.05v38M1105 661.05v38M65 699.05v38" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M897 548v38M1105 548v38M65 586v38" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 586v38" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 586v38" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 586v38" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 586v38M1105 586v38M65 624v75.05" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 624v75.05" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 624v75.05" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 624v75.05" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 624v75.05M1105 624v75.05M65 699.05v38" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 699.05v38" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 699.05v38" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 699.05v38" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 699.05v38M1105 699.05v38M65 737.05v75.05" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 737.05v75.05" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 737.05v75.05" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 737.05v75.05" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 737.05v75.05M1105 737.05v75.05M65 812.1v38" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 812.1v38" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 812.1v38" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 812.1v38" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 812.1v38M1105 812.1v38M65 850.1v38" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M897 699.05v38M1105 699.05v38M65 737.05v38" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 737.05v38" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 737.05v38" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 737.05v38" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 737.05v38M1105 737.05v38M65 775.05v75.05" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 775.05v75.05" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 775.05v75.05" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 775.05v75.05" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 775.05v75.05M1105 775.05v75.05M65 850.1v38" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 850.1v38" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 850.1v38" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 850.1v38" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 850.1v38M1105 850.1v38M65 888.1v86" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 888.1v86" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 888.1v86" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 888.1v86" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 888.1v86M1105 888.1v86M65 974.1v38" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 974.1v38" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 974.1v38" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 974.1v38" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 974.1v38M1105 974.1v38M65 1012.1v38" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M897 850.1v38M1105 850.1v38M65 888.1v38" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 888.1v38" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 888.1v38" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 888.1v38" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 888.1v38M1105 888.1v38M65 926.1v86" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 926.1v86" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 926.1v86" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 926.1v86" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 926.1v86M1105 926.1v86M65 1012.1v38" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 1012.1v38" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 1012.1v38" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 1012.1v38" class="arcrow" style="stroke:#00f"/>
|
||||
@@ -149,15 +149,15 @@
|
||||
<path d="M273 1164.1v38" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 1164.1v38" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 1164.1v38" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 1164.1v38M1105 1164.1v38M65 1202.1v75.05" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 1202.1v75.05" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 1202.1v75.05" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 1202.1v75.05" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 1202.1v75.05M1105 1202.1v75.05M65 1277.15v38" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 1277.15v38" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 1277.15v38" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 1277.15v38" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 1277.15v38M1105 1277.15v38M65 1315.15v38" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M897 1164.1v38M1105 1164.1v38M65 1202.1v38" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 1202.1v38" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 1202.1v38" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 1202.1v38" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 1202.1v38M1105 1202.1v38M65 1240.1v75.05" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 1240.1v75.05" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 1240.1v75.05" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 1240.1v75.05" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 1240.1v75.05M1105 1240.1v75.05M65 1315.15v38" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 1315.15v38" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 1315.15v38" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 1315.15v38" class="arcrow" style="stroke:#00f"/>
|
||||
@@ -173,19 +173,19 @@
|
||||
<path d="M273 1429.15v38" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 1429.15v38" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 1429.15v38" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 1429.15v38M1105 1429.15v38M65 1467.15v59.05" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 1467.15v59.05" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 1467.15v59.05" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 1467.15v59.05" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 1467.15v59.05M1105 1467.15v59.05M65 1526.2v54" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 1526.2v54" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 1526.2v54" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 1526.2v54" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 1526.2v54M1105 1526.2v54M65 1580.2v38" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 1580.2v38" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 1580.2v38" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 1580.2v38" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 1580.2v38M1105 1580.2v38M65 1618.2v38" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M897 1429.15v38M1105 1429.15v38M65 1467.15v38" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 1467.15v38" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 1467.15v38" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 1467.15v38" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 1467.15v38M1105 1467.15v38M65 1505.15v59.05" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 1505.15v59.05" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 1505.15v59.05" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 1505.15v59.05" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 1505.15v59.05M1105 1505.15v59.05M65 1564.2v54" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 1564.2v54" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 1564.2v54" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 1564.2v54" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 1564.2v54M1105 1564.2v54M65 1618.2v38" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 1618.2v38" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 1618.2v38" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 1618.2v38" class="arcrow" style="stroke:#00f"/>
|
||||
@@ -209,15 +209,15 @@
|
||||
<path d="M273 1808.2v38" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 1808.2v38" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 1808.2v38" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 1808.2v38M1105 1808.2v38M65 1846.2v59.05" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 1846.2v59.05" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 1846.2v59.05" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 1846.2v59.05" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 1846.2v59.05M1105 1846.2v59.05M65 1905.25v38" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 1905.25v38" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 1905.25v38" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 1905.25v38" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 1905.25v38M1105 1905.25v38M65 1943.25v38" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M897 1808.2v38M1105 1808.2v38M65 1846.2v38" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 1846.2v38" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 1846.2v38" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 1846.2v38" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 1846.2v38M1105 1846.2v38M65 1884.2v59.05" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 1884.2v59.05" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 1884.2v59.05" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 1884.2v59.05" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 1884.2v59.05M1105 1884.2v59.05M65 1943.25v38" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 1943.25v38" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 1943.25v38" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 1943.25v38" class="arcrow" style="stroke:#00f"/>
|
||||
@@ -229,7 +229,11 @@
|
||||
<path d="M273 2019.25v38" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 2019.25v38" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 2019.25v38" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 2019.25v38M1105 2019.25v38" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M897 2019.25v38M1105 2019.25v38M65 2057.25v38" class="arcrow" style="stroke:transparent"/>
|
||||
<path d="M273 2057.25v38" class="arcrow" style="stroke:#080"/>
|
||||
<path d="M481 2057.25v38" class="arcrow" style="stroke:red"/>
|
||||
<path d="M689 2057.25v38" class="arcrow" style="stroke:#00f"/>
|
||||
<path d="M897 2057.25v38M1105 2057.25v38" class="arcrow" style="stroke:transparent"/>
|
||||
</g>
|
||||
<g id="mscgenjsreplaceme_sequence">
|
||||
<path d="M0 0h130v38H0z" class="entity" style="stroke:transparent"/>
|
||||
@@ -268,85 +272,85 @@
|
||||
<path marker-end="url(#mscgenjsreplacemecallback-#0000FF)" d="M689 529H273" class="arc directional callback" style="stroke:#00f"/>
|
||||
<path d="M415.64 513.25h130.72v14H415.64z" class="label-text-background"/>
|
||||
<text x="481" y="524.25" class="directional-text callback-text"><tspan>Sends script-coin-lock-tx</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacememethod-#FF0000)" d="M481 615.92c104 .1 104 22.8 0 22.8" class="arc directional method" style="stroke:red"/>
|
||||
<path d="M484 567.67h40.91v14H484z" class="label-text-background"/>
|
||||
<text x="484" y="578.67" class="directional-text method-text anchor-start"><tspan>Wait for</tspan></text>
|
||||
<path d="M484 583.67h107.01v14H484z" class="label-text-background"/>
|
||||
<text x="484" y="594.67" class="directional-text method-text anchor-start"><tspan>script-coin-lock-tx to</tspan></text>
|
||||
<path d="M484 599.67h39.34v14H484z" class="label-text-background"/>
|
||||
<text x="484" y="610.67" class="directional-text method-text anchor-start"><tspan>confirm</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacemecallback-#FF0000)" d="M481 718.05H273" class="arc directional callback" style="stroke:red"/>
|
||||
<path d="M304.97 702.3h144.06v14H304.97z" class="label-text-background"/>
|
||||
<text x="377" y="713.3" class="directional-text callback-text"><tspan>Sends noscript-coin-lock-tx</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacememethod-#FF0000)" d="M481 766.97c104 .1 104 22.8 0 22.8" class="arc directional method" style="stroke:red"/>
|
||||
<path d="M484 718.72h40.91v14H484z" class="label-text-background"/>
|
||||
<text x="484" y="729.72" class="directional-text method-text anchor-start"><tspan>Wait for</tspan></text>
|
||||
<path d="M484 734.72h120.36v14H484z" class="label-text-background"/>
|
||||
<text x="484" y="745.72" class="directional-text method-text anchor-start"><tspan>noscript-coin-lock-tx to</tspan></text>
|
||||
<path d="M484 750.72h39.34v14H484z" class="label-text-background"/>
|
||||
<text x="484" y="761.72" class="directional-text method-text anchor-start"><tspan>confirm</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacememethod-#0000FF)" d="M689 766.97c104 .1 104 22.8 0 22.8" class="arc directional method" style="stroke:#00f"/>
|
||||
<path d="M692 718.72h40.91v14H692z" class="label-text-background"/>
|
||||
<text x="692" y="729.72" class="directional-text method-text anchor-start"><tspan>Wait for</tspan></text>
|
||||
<path d="M692 734.72h120.36v14H692z" class="label-text-background"/>
|
||||
<text x="692" y="745.72" class="directional-text method-text anchor-start"><tspan>noscript-coin-lock-tx to</tspan></text>
|
||||
<path d="M692 750.72h39.34v14H692z" class="label-text-background"/>
|
||||
<text x="692" y="761.72" class="directional-text method-text anchor-start"><tspan>confirm</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacememethod-#0000FF)" d="M689 931.1H481" class="arc directional method" style="stroke:#00f"/>
|
||||
<path d="M519.64 915.35h130.72v14H519.64z" class="label-text-background"/>
|
||||
<text x="585" y="926.35" class="directional-text method-text"><tspan>Sends script-coin-lock-tx</tspan></text>
|
||||
<path d="M539.3 933.35h91.71v14H539.3z" class="label-text-background"/>
|
||||
<text x="585" y="944.35" class="directional-text method-text"><tspan>release message</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacemecallback-#FF0000)" d="M481 1031.1H273" class="arc directional callback" style="stroke:red"/>
|
||||
<path d="M293.3 1015.35h167.41v14H293.3z" class="label-text-background"/>
|
||||
<text x="377" y="1026.35" class="directional-text callback-text"><tspan>Sends script-coin-lock-spend-tx</tspan></text>
|
||||
<path d="M-41 1145.1h1044" class="inline_expression_divider"/>
|
||||
<path d="M459.98 1137.85h42.03v14h-42.03z" class="label-text-background"/>
|
||||
<text x="481" y="1148.85" class="empty-text comment-row-text"><tspan>fail path</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacememethod-#FF0000)" d="M481 1232.02c104 .1 104 22.8 0 22.8" class="arc directional method" style="stroke:red"/>
|
||||
<path d="M484 1183.77h40.91v14H484z" class="label-text-background"/>
|
||||
<text x="484" y="1194.77" class="directional-text method-text anchor-start"><tspan>Wait for</tspan></text>
|
||||
<path d="M484 1199.77h131.69v14H484z" class="label-text-background"/>
|
||||
<text x="484" y="1210.77" class="directional-text method-text anchor-start"><tspan>script-coin-lock-tx lock to</tspan></text>
|
||||
<path d="M484 1215.77h33.01v14H484z" class="label-text-background"/>
|
||||
<text x="484" y="1226.77" class="directional-text method-text anchor-start"><tspan>expire</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacemecallback-#0000FF)" d="M689 1296.15H273" class="arc directional callback" style="stroke:#00f"/>
|
||||
<path d="M385.96 1280.4h190.08v14H385.96z" class="label-text-background"/>
|
||||
<text x="481" y="1291.4" class="directional-text callback-text"><tspan>Sends script-coin-lock-pre-refund-tx</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacemecallback-#008800)" d="M273 1334.15h416" class="arc directional return" style="stroke:#080"/>
|
||||
<path d="M404.64 1318.4h152.72v14H404.64z" class="label-text-background"/>
|
||||
<text x="481" y="1329.4" class="directional-text return-text"><tspan>script-coin-lock-pre-refund-tx</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacememethod-#0000FF)" d="M689 1489.07c104 .1 104 22.8 0 22.8" class="arc directional method" style="stroke:#00f"/>
|
||||
<path d="M692 1456.82h40.91v14H692z" class="label-text-background"/>
|
||||
<text x="692" y="1467.82" class="directional-text method-text anchor-start"><tspan>Wait for</tspan></text>
|
||||
<path d="M692 1472.82h124.06v14H692z" class="label-text-background"/>
|
||||
<text x="692" y="1483.82" class="directional-text method-text anchor-start"><tspan>pre-refund tx to confirm</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacemecallback-#0000FF)" d="M689 1553.2H273" class="arc directional callback" style="stroke:#00f"/>
|
||||
<path d="M367.62 1537.45h226.77v14H367.62z" class="label-text-background"/>
|
||||
<text x="481" y="1548.45" class="directional-text callback-text"><tspan>Sends script-coin-lock-pre-refund-spend-tx</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacemecallback-#008800)" d="M273 1637.2h208" class="arc directional return" style="stroke:#080"/>
|
||||
<path d="M356.66 1621.45h40.69v14h-40.69z" class="label-text-background"/>
|
||||
<text x="377" y="1632.45" class="directional-text return-text"><tspan>Detects</tspan></text>
|
||||
<path d="M282.3 1639.45h189.41v14H282.3z" class="label-text-background"/>
|
||||
<text x="377" y="1650.45" class="directional-text return-text"><tspan>script-coin-lock-pre-refund-spend-tx</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacemecallback-#FF0000)" d="M481 1675.2H273" class="arc directional callback" style="stroke:red"/>
|
||||
<path d="M359.98 1659.45h34.03v14h-34.03z" class="label-text-background"/>
|
||||
<text x="377" y="1670.45" class="directional-text callback-text"><tspan>Sends</tspan></text>
|
||||
<path d="M297.65 1677.45h158.7v14h-158.7z" class="label-text-background"/>
|
||||
<text x="377" y="1688.45" class="directional-text callback-text"><tspan>scriptless-coin-lock-recover-tx</tspan></text>
|
||||
<path d="M-37 1789.2H999" class="inline_expression_divider"/>
|
||||
<path d="M396.41 1781.95h169.17v14H396.41z" class="label-text-background"/>
|
||||
<text x="481" y="1792.95" class="empty-text comment-row-text"><tspan>offerer swipes script coin lock tx</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacememethod-#FF0000)" d="M481 1868.12c104 .1 104 22.8 0 22.8" class="arc directional method" style="stroke:red"/>
|
||||
<path d="M484 1835.87h40.91v14H484z" class="label-text-background"/>
|
||||
<text x="484" y="1846.87" class="directional-text method-text anchor-start"><tspan>Wait for</tspan></text>
|
||||
<path d="M484 1851.87h142.39v14H484z" class="label-text-background"/>
|
||||
<text x="484" y="1862.87" class="directional-text method-text anchor-start"><tspan>pre-refund tx lock to expire</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacemecallback-#FF0000)" d="M481 1924.25H273" class="arc directional callback" style="stroke:red"/>
|
||||
<path d="M359.98 1908.5h34.03v14h-34.03z" class="label-text-background"/>
|
||||
<text x="377" y="1919.5" class="directional-text callback-text"><tspan>Sends</tspan></text>
|
||||
<path d="M283.3 1926.5h187.39v14H283.3z" class="label-text-background"/>
|
||||
<text x="377" y="1937.5" class="directional-text callback-text"><tspan>script-coin-lock-pre-refund-swipe-tx</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacememethod-#FF0000)" d="M481 653.92c104 .1 104 22.8 0 22.8" class="arc directional method" style="stroke:red"/>
|
||||
<path d="M484 605.67h40.91v14H484z" class="label-text-background"/>
|
||||
<text x="484" y="616.67" class="directional-text method-text anchor-start"><tspan>Wait for</tspan></text>
|
||||
<path d="M484 621.67h107.01v14H484z" class="label-text-background"/>
|
||||
<text x="484" y="632.67" class="directional-text method-text anchor-start"><tspan>script-coin-lock-tx to</tspan></text>
|
||||
<path d="M484 637.67h39.34v14H484z" class="label-text-background"/>
|
||||
<text x="484" y="648.67" class="directional-text method-text anchor-start"><tspan>confirm</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacemecallback-#FF0000)" d="M481 756.05H273" class="arc directional callback" style="stroke:red"/>
|
||||
<path d="M304.97 740.3h144.06v14H304.97z" class="label-text-background"/>
|
||||
<text x="377" y="751.3" class="directional-text callback-text"><tspan>Sends noscript-coin-lock-tx</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacememethod-#FF0000)" d="M481 804.97c104 .1 104 22.8 0 22.8" class="arc directional method" style="stroke:red"/>
|
||||
<path d="M484 756.72h40.91v14H484z" class="label-text-background"/>
|
||||
<text x="484" y="767.72" class="directional-text method-text anchor-start"><tspan>Wait for</tspan></text>
|
||||
<path d="M484 772.72h120.36v14H484z" class="label-text-background"/>
|
||||
<text x="484" y="783.72" class="directional-text method-text anchor-start"><tspan>noscript-coin-lock-tx to</tspan></text>
|
||||
<path d="M484 788.72h39.34v14H484z" class="label-text-background"/>
|
||||
<text x="484" y="799.72" class="directional-text method-text anchor-start"><tspan>confirm</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacememethod-#0000FF)" d="M689 804.97c104 .1 104 22.8 0 22.8" class="arc directional method" style="stroke:#00f"/>
|
||||
<path d="M692 756.72h40.91v14H692z" class="label-text-background"/>
|
||||
<text x="692" y="767.72" class="directional-text method-text anchor-start"><tspan>Wait for</tspan></text>
|
||||
<path d="M692 772.72h120.36v14H692z" class="label-text-background"/>
|
||||
<text x="692" y="783.72" class="directional-text method-text anchor-start"><tspan>noscript-coin-lock-tx to</tspan></text>
|
||||
<path d="M692 788.72h39.34v14H692z" class="label-text-background"/>
|
||||
<text x="692" y="799.72" class="directional-text method-text anchor-start"><tspan>confirm</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacememethod-#0000FF)" d="M689 969.1H481" class="arc directional method" style="stroke:#00f"/>
|
||||
<path d="M519.64 953.35h130.72v14H519.64z" class="label-text-background"/>
|
||||
<text x="585" y="964.35" class="directional-text method-text"><tspan>Sends script-coin-lock-tx</tspan></text>
|
||||
<path d="M539.3 971.35h91.71v14H539.3z" class="label-text-background"/>
|
||||
<text x="585" y="982.35" class="directional-text method-text"><tspan>release message</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacemecallback-#FF0000)" d="M481 1069.1H273" class="arc directional callback" style="stroke:red"/>
|
||||
<path d="M293.3 1053.35h167.41v14H293.3z" class="label-text-background"/>
|
||||
<text x="377" y="1064.35" class="directional-text callback-text"><tspan>Sends script-coin-lock-spend-tx</tspan></text>
|
||||
<path d="M-41 1183.1h1044" class="inline_expression_divider"/>
|
||||
<path d="M459.98 1175.85h42.03v14h-42.03z" class="label-text-background"/>
|
||||
<text x="481" y="1186.85" class="empty-text comment-row-text"><tspan>fail path</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacememethod-#FF0000)" d="M481 1270.02c104 .1 104 22.8 0 22.8" class="arc directional method" style="stroke:red"/>
|
||||
<path d="M484 1221.77h40.91v14H484z" class="label-text-background"/>
|
||||
<text x="484" y="1232.77" class="directional-text method-text anchor-start"><tspan>Wait for</tspan></text>
|
||||
<path d="M484 1237.77h131.69v14H484z" class="label-text-background"/>
|
||||
<text x="484" y="1248.77" class="directional-text method-text anchor-start"><tspan>script-coin-lock-tx lock to</tspan></text>
|
||||
<path d="M484 1253.77h33.01v14H484z" class="label-text-background"/>
|
||||
<text x="484" y="1264.77" class="directional-text method-text anchor-start"><tspan>expire</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacemecallback-#0000FF)" d="M689 1334.15H273" class="arc directional callback" style="stroke:#00f"/>
|
||||
<path d="M385.96 1318.4h190.08v14H385.96z" class="label-text-background"/>
|
||||
<text x="481" y="1329.4" class="directional-text callback-text"><tspan>Sends script-coin-lock-pre-refund-tx</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacemecallback-#008800)" d="M273 1372.15h416" class="arc directional return" style="stroke:#080"/>
|
||||
<path d="M404.64 1356.4h152.72v14H404.64z" class="label-text-background"/>
|
||||
<text x="481" y="1367.4" class="directional-text return-text"><tspan>script-coin-lock-pre-refund-tx</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacememethod-#0000FF)" d="M689 1527.07c104 .1 104 22.8 0 22.8" class="arc directional method" style="stroke:#00f"/>
|
||||
<path d="M692 1494.82h40.91v14H692z" class="label-text-background"/>
|
||||
<text x="692" y="1505.82" class="directional-text method-text anchor-start"><tspan>Wait for</tspan></text>
|
||||
<path d="M692 1510.82h124.06v14H692z" class="label-text-background"/>
|
||||
<text x="692" y="1521.82" class="directional-text method-text anchor-start"><tspan>pre-refund tx to confirm</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacemecallback-#0000FF)" d="M689 1591.2H273" class="arc directional callback" style="stroke:#00f"/>
|
||||
<path d="M367.62 1575.45h226.77v14H367.62z" class="label-text-background"/>
|
||||
<text x="481" y="1586.45" class="directional-text callback-text"><tspan>Sends script-coin-lock-pre-refund-spend-tx</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacemecallback-#008800)" d="M273 1675.2h208" class="arc directional return" style="stroke:#080"/>
|
||||
<path d="M356.66 1659.45h40.69v14h-40.69z" class="label-text-background"/>
|
||||
<text x="377" y="1670.45" class="directional-text return-text"><tspan>Detects</tspan></text>
|
||||
<path d="M282.3 1677.45h189.41v14H282.3z" class="label-text-background"/>
|
||||
<text x="377" y="1688.45" class="directional-text return-text"><tspan>script-coin-lock-pre-refund-spend-tx</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacemecallback-#FF0000)" d="M481 1713.2H273" class="arc directional callback" style="stroke:red"/>
|
||||
<path d="M359.98 1697.45h34.03v14h-34.03z" class="label-text-background"/>
|
||||
<text x="377" y="1708.45" class="directional-text callback-text"><tspan>Sends</tspan></text>
|
||||
<path d="M297.65 1715.45h158.7v14h-158.7z" class="label-text-background"/>
|
||||
<text x="377" y="1726.45" class="directional-text callback-text"><tspan>scriptless-coin-lock-recover-tx</tspan></text>
|
||||
<path d="M-37 1827.2H999" class="inline_expression_divider"/>
|
||||
<path d="M396.41 1819.95h169.17v14H396.41z" class="label-text-background"/>
|
||||
<text x="481" y="1830.95" class="empty-text comment-row-text"><tspan>offerer swipes script coin lock tx</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacememethod-#FF0000)" d="M481 1906.12c104 .1 104 22.8 0 22.8" class="arc directional method" style="stroke:red"/>
|
||||
<path d="M484 1873.87h40.91v14H484z" class="label-text-background"/>
|
||||
<text x="484" y="1884.87" class="directional-text method-text anchor-start"><tspan>Wait for</tspan></text>
|
||||
<path d="M484 1889.87h142.39v14H484z" class="label-text-background"/>
|
||||
<text x="484" y="1900.87" class="directional-text method-text anchor-start"><tspan>pre-refund tx lock to expire</tspan></text>
|
||||
<path marker-end="url(#mscgenjsreplacemecallback-#FF0000)" d="M481 1962.25H273" class="arc directional callback" style="stroke:red"/>
|
||||
<path d="M359.98 1946.5h34.03v14h-34.03z" class="label-text-background"/>
|
||||
<text x="377" y="1957.5" class="directional-text callback-text"><tspan>Sends</tspan></text>
|
||||
<path d="M283.3 1964.5h187.39v14H283.3z" class="label-text-background"/>
|
||||
<text x="377" y="1975.5" class="directional-text callback-text"><tspan>script-coin-lock-pre-refund-swipe-tx</tspan></text>
|
||||
</g>
|
||||
<g id="mscgenjsreplaceme_notes">
|
||||
<path d="m383 209 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
|
||||
@@ -371,50 +375,50 @@
|
||||
<path d="M799 474h395v9h9m-9-9 9 9v25H799v-34z" class="box note" style="fill:#ffc"/>
|
||||
<text x="1001" y="486.75" class="box-text note-text"><tspan>The XmrBidLockSpendTxMessage contains the script-coin-lock-tx and</tspan></text>
|
||||
<text x="1001" y="502.75" class="box-text note-text"><tspan>proof the bidder can sign it.</tspan></text>
|
||||
<path d="m383 529 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
|
||||
<text x="481" y="532.75" class="box-text abox-text"><tspan>Bid Script coin spend tx valid</tspan></text>
|
||||
<path d="m383 567 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
|
||||
<text x="481" y="562.75" class="box-text abox-text"><tspan>Exchanged script lock spend tx</tspan></text>
|
||||
<text x="481" y="578.75" class="box-text abox-text"><tspan>msg</tspan></text>
|
||||
<path d="m383 680.05 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
|
||||
<text x="481" y="683.8" class="box-text abox-text"><tspan>Bid Script coin locked</tspan></text>
|
||||
<path d="m383 831.1 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
|
||||
<text x="481" y="834.85" class="box-text abox-text"><tspan>Bid Scriptless coin locked</tspan></text>
|
||||
<path d="M-40 869.1h98.39v11l-7 7H-40" class="box inline_expression_label"/>
|
||||
<text x="-38" y="882.35" class="inline_expression-text alt-text anchor-start"><tspan>alt: success path</tspan></text>
|
||||
<path d="M799 890.1h395v9h9m-9-9 9 9v73H799v-82z" class="box note" style="fill:#ffc"/>
|
||||
<text x="1001" y="902.85" class="box-text note-text"><tspan>The XmrBidLockReleaseMessage contains the bidder's OTVES for it. </tspan></text>
|
||||
<text x="1001" y="918.85" class="box-text note-text"><tspan> The offerer decodes the bidder's signature</tspan></text>
|
||||
<text x="1001" y="934.85" class="box-text note-text"><tspan>from the OTVES. When the bidder has the</tspan></text>
|
||||
<text x="1001" y="950.85" class="box-text note-text"><tspan>plaintext signature, they can decode the offerer's noscript-coin-lock-tx</tspan></text>
|
||||
<text x="1001" y="966.85" class="box-text note-text"><tspan>signature.</tspan></text>
|
||||
<path d="m383 993.1 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
|
||||
<text x="481" y="996.85" class="box-text abox-text"><tspan>Script coin lock released</tspan></text>
|
||||
<path d="m383 1069.1 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
|
||||
<text x="481" y="1072.85" class="box-text abox-text"><tspan>Script tx redeemed</tspan></text>
|
||||
<text x="481" y="570.75" class="box-text abox-text"><tspan>Bid Script coin spend tx valid</tspan></text>
|
||||
<path d="m383 605 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
|
||||
<text x="481" y="600.75" class="box-text abox-text"><tspan>Exchanged script lock spend tx</tspan></text>
|
||||
<text x="481" y="616.75" class="box-text abox-text"><tspan>msg</tspan></text>
|
||||
<path d="m383 718.05 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
|
||||
<text x="481" y="721.8" class="box-text abox-text"><tspan>Bid Script coin locked</tspan></text>
|
||||
<path d="m383 869.1 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
|
||||
<text x="481" y="872.85" class="box-text abox-text"><tspan>Bid Scriptless coin locked</tspan></text>
|
||||
<path d="M-40 907.1h98.39v11l-7 7H-40" class="box inline_expression_label"/>
|
||||
<text x="-38" y="920.35" class="inline_expression-text alt-text anchor-start"><tspan>alt: success path</tspan></text>
|
||||
<path d="M799 928.1h395v9h9m-9-9 9 9v73H799v-82z" class="box note" style="fill:#ffc"/>
|
||||
<text x="1001" y="940.85" class="box-text note-text"><tspan>The XmrBidLockReleaseMessage contains the bidder's OTVES for it. </tspan></text>
|
||||
<text x="1001" y="956.85" class="box-text note-text"><tspan> The offerer decodes the bidder's signature</tspan></text>
|
||||
<text x="1001" y="972.85" class="box-text note-text"><tspan>from the OTVES. When the bidder has the</tspan></text>
|
||||
<text x="1001" y="988.85" class="box-text note-text"><tspan>plaintext signature, they can decode the offerer's noscript-coin-lock-tx</tspan></text>
|
||||
<text x="1001" y="1004.85" class="box-text note-text"><tspan>signature.</tspan></text>
|
||||
<path d="m383 1031.1 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
|
||||
<text x="481" y="1034.85" class="box-text abox-text"><tspan>Script coin lock released</tspan></text>
|
||||
<path d="m383 1107.1 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
|
||||
<text x="481" y="1110.85" class="box-text abox-text"><tspan>Bid Completed</tspan></text>
|
||||
<path d="M799 1279.15h395v9h9m-9-9 9 9v25H799v-34z" class="box note" style="fill:#ffc"/>
|
||||
<text x="1001" y="1299.9" class="box-text note-text"><tspan>tx can be sent by either party.</tspan></text>
|
||||
<path d="m383 1372.15 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
|
||||
<text x="481" y="1367.9" class="box-text abox-text"><tspan>Bid Script pre-refund tx in</tspan></text>
|
||||
<text x="481" y="1383.9" class="box-text abox-text"><tspan>chain</tspan></text>
|
||||
<path d="M-36 1410.15h199.77v11l-7 7H-36" class="box inline_expression_label"/>
|
||||
<text x="-34" y="1423.4" class="inline_expression-text alt-text anchor-start"><tspan>alt: bidder refunds script coin lock tx</tspan></text>
|
||||
<path d="M799 1528.2h395v9h9m-9-9 9 9v41H799v-50z" class="box note" style="fill:#ffc"/>
|
||||
<text x="1001" y="1540.95" class="box-text note-text"><tspan>Refunds the script lock tx, with the bidder's cleartext signature</tspan></text>
|
||||
<text x="1001" y="1556.95" class="box-text note-text"><tspan>the offerer can refund the noscript lock tx. </tspan></text>
|
||||
<text x="1001" y="1572.95" class="box-text note-text"><tspan>Once the lock expires the pre-refund tx can be spent by the offerer.</tspan></text>
|
||||
<path d="m591 1599.2 3-17h190l3 17-3 17H594z" class="box abox" style="stroke:#00f"/>
|
||||
<text x="689" y="1602.95" class="box-text abox-text"><tspan>Bid Failed, refunded</tspan></text>
|
||||
<path d="M799 1620.2h395v9h9m-9-9 9 9v25H799v-34z" class="box note" style="fill:#ffc"/>
|
||||
<text x="1001" y="1640.95" class="box-text note-text"><tspan>offerer recovers the bidder's scriptless chain key-shard.</tspan></text>
|
||||
<path d="m383 1713.2 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
|
||||
<text x="481" y="1716.95" class="box-text abox-text"><tspan>Bid Scriptless tx recovered</tspan></text>
|
||||
<text x="481" y="1110.85" class="box-text abox-text"><tspan>Script tx redeemed</tspan></text>
|
||||
<path d="m383 1145.1 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
|
||||
<text x="481" y="1148.85" class="box-text abox-text"><tspan>Bid Completed</tspan></text>
|
||||
<path d="M799 1317.15h395v9h9m-9-9 9 9v25H799v-34z" class="box note" style="fill:#ffc"/>
|
||||
<text x="1001" y="1337.9" class="box-text note-text"><tspan>tx can be sent by either party.</tspan></text>
|
||||
<path d="m383 1410.15 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
|
||||
<text x="481" y="1405.9" class="box-text abox-text"><tspan>Bid Script pre-refund tx in</tspan></text>
|
||||
<text x="481" y="1421.9" class="box-text abox-text"><tspan>chain</tspan></text>
|
||||
<path d="M-36 1448.15h199.77v11l-7 7H-36" class="box inline_expression_label"/>
|
||||
<text x="-34" y="1461.4" class="inline_expression-text alt-text anchor-start"><tspan>alt: bidder refunds script coin lock tx</tspan></text>
|
||||
<path d="M799 1566.2h395v9h9m-9-9 9 9v41H799v-50z" class="box note" style="fill:#ffc"/>
|
||||
<text x="1001" y="1578.95" class="box-text note-text"><tspan>Refunds the script lock tx, with the bidder's cleartext signature</tspan></text>
|
||||
<text x="1001" y="1594.95" class="box-text note-text"><tspan>the offerer can refund the noscript lock tx. </tspan></text>
|
||||
<text x="1001" y="1610.95" class="box-text note-text"><tspan>Once the lock expires the pre-refund tx can be spent by the offerer.</tspan></text>
|
||||
<path d="m591 1637.2 3-17h190l3 17-3 17H594z" class="box abox" style="stroke:#00f"/>
|
||||
<text x="689" y="1640.95" class="box-text abox-text"><tspan>Bid Failed, refunded</tspan></text>
|
||||
<path d="M799 1658.2h395v9h9m-9-9 9 9v25H799v-34z" class="box note" style="fill:#ffc"/>
|
||||
<text x="1001" y="1678.95" class="box-text note-text"><tspan>offerer recovers the bidder's scriptless chain key-shard.</tspan></text>
|
||||
<path d="m383 1751.2 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
|
||||
<text x="481" y="1754.95" class="box-text abox-text"><tspan>Bid Failed, refunded</tspan></text>
|
||||
<path d="m383 1962.25 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
|
||||
<text x="481" y="1966" class="box-text abox-text"><tspan>Bid Failed, swiped</tspan></text>
|
||||
<text x="481" y="1754.95" class="box-text abox-text"><tspan>Bid Scriptless tx recovered</tspan></text>
|
||||
<path d="m383 1789.2 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
|
||||
<text x="481" y="1792.95" class="box-text abox-text"><tspan>Bid Failed, refunded</tspan></text>
|
||||
<path d="m383 2000.25 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
|
||||
<text x="481" y="2004" class="box-text abox-text"><tspan>Bid Failed, swiped</tspan></text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 40 KiB |
@@ -1,4 +1,5 @@
|
||||
{% include 'header.html' %}
|
||||
{% from 'style.html' import breadcrumb_line_svg %}
|
||||
<div class="container mx-auto">
|
||||
<section class="p-5 mt-5">
|
||||
<div class="flex flex-wrap items-center -m-2">
|
||||
@@ -9,19 +10,11 @@
|
||||
<p>Home</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<svg width="6" height="15" viewBox="0 0 6 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.34 0.671999L2.076 14.1H0.732L3.984 0.671999H5.34Z" fill="#BBC3CF"></path>
|
||||
</svg>
|
||||
</li>
|
||||
<li>{{ breadcrumb_line_svg | safe }}</li>
|
||||
<li>
|
||||
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/404">404</a>
|
||||
</li>
|
||||
<li>
|
||||
<svg width="6" height="15" viewBox="0 0 6 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.34 0.671999L2.076 14.1H0.732L3.984 0.671999H5.34Z" fill="#BBC3CF"></path>
|
||||
</svg>
|
||||
</li>
|
||||
<li>{{ breadcrumb_line_svg | safe }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,7 +30,6 @@
|
||||
<div class="flex flex-wrap">
|
||||
<div class="w-full lg:w-auto py-1 lg:py-0 lg:mr-6"><a class="inline-block py-5 px-7 w-full text-base md:text-lg leading-4 text-blue-50 font-medium text-center bg-blue-500 hover:bg-blue-600 border border-blue-500 rounded-md shadow-sm focus:ring-0 focus:outline-none" href="/">Go Back</a></div>
|
||||
<div class="w-full lg:w-auto py-1 lg:py-0"><a class="inline-block py-5 px-7 w-full text-base md:text-lg leading-4 text-coolGray-800 font-medium text-center bg-white hover:bg-coolGray-100 border border-coolGray-200 rounded-md shadow-sm focus:ring-0 focus:outline-none" href="/refresh">Try Again</a></div>
|
||||
<!-- todo add last URL page visit -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% include 'header.html' %}
|
||||
{% from 'style.html' import breadcrumb_line_svg, circular_arrows_svg %}
|
||||
<div class="container mx-auto">
|
||||
<section class="p-5 mt-5">
|
||||
<div class="flex flex-wrap items-center -m-2">
|
||||
@@ -9,19 +10,11 @@
|
||||
<p>Home</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<svg width="6" height="15" viewBox="0 0 6 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.34 0.671999L2.076 14.1H0.732L3.984 0.671999H5.34Z" fill="#BBC3CF"></path>
|
||||
</svg>
|
||||
</li>
|
||||
<li>{{ breadcrumb_line_svg | safe }}</li>
|
||||
<li>
|
||||
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/active">Swaps In Progress</a>
|
||||
</li>
|
||||
<li>
|
||||
<svg width="6" height="15" viewBox="0 0 6 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.34 0.671999L2.076 14.1H0.732L3.984 0.671999H5.34Z" fill="#BBC3CF"></path>
|
||||
</svg>
|
||||
</li>
|
||||
<li>{{ breadcrumb_line_svg | safe }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,23 +32,13 @@
|
||||
</div>
|
||||
<div class="w-full md:w-1/2 p-3 p-6 container flex flex-wrap items-center justify-end items-center mx-auto">
|
||||
{% if refresh %}
|
||||
<a id="refresh" href="/active" class="flex flex-wrap justify-center px-5 py-4 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white borderdark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
|
||||
<svg class="text-gray-500 w-5 h-5 mr-2" xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
|
||||
<g fill="#ffffff">
|
||||
<path fill="#ffffff" d="M12,3c1.989,0,3.873,0.65,5.43,1.833l-3.604,3.393l9.167,0.983L22.562,0l-3.655,3.442 C16.957,1.862,14.545,1,12,1C5.935,1,1,5.935,1,12h2C3,7.037,7.037,3,12,3z"></path>
|
||||
<path data-color="color-2" d="M12,21c-1.989,0-3.873-0.65-5.43-1.833l3.604-3.393l-9.167-0.983L1.438,24l3.655-3.442 C7.043,22.138,9.455,23,12,23c6.065,0,11-4.935,11-11h-2C21,16.963,16.963,21,12,21z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
<a id="refresh" href="/active" class="rounded-full flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border dark:bg-gray-500 dark:hover:bg-gray-700 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
|
||||
{{ circular_arrows_svg | safe }}
|
||||
<span>Refresh 30 seconds</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<a id="refresh" href="/active" class="flex flex-wrap justify-center px-5 py-4 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white borderdark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
|
||||
<svg class="text-gray-500 w-5 h-5 mr-2" xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
|
||||
<g fill="#ffffff">
|
||||
<path fill="#ffffff" d="M12,3c1.989,0,3.873,0.65,5.43,1.833l-3.604,3.393l9.167,0.983L22.562,0l-3.655,3.442 C16.957,1.862,14.545,1,12,1C5.935,1,1,5.935,1,12h2C3,7.037,7.037,3,12,3z"></path>
|
||||
<path data-color="color-2" d="M12,21c-1.989,0-3.873-0.65-5.43-1.833l3.604-3.393l-9.167-0.983L1.438,24l3.655-3.442 C7.043,22.138,9.455,23,12,23c6.065,0,11-4.935,11-11h-2C21,16.963,16.963,21,12,21z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
{{ circular_arrows_svg | safe }}
|
||||
<span>Refresh</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -83,22 +66,22 @@
|
||||
</th>
|
||||
<th class="p-0">
|
||||
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600">
|
||||
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Offer ID </span>
|
||||
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Offer ID</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="p-0">
|
||||
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600">
|
||||
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Bid Status </span>
|
||||
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Bid Status</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="p-0">
|
||||
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600">
|
||||
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">ITX Status </span>
|
||||
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">ITX Status</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="p-0">
|
||||
<div class="py-3 px-6 rounded-tr-xl bg-coolGray-200 dark:bg-gray-600">
|
||||
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">PTX Status </span>
|
||||
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">PTX Status</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
|
||||