tx-engine 0.11.2__tar.gz → 0.11.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tx_engine-0.11.2 → tx_engine-0.11.3}/.github/workflows/CI.yml +13 -13
- {tx_engine-0.11.2 → tx_engine-0.11.3}/.github/workflows/on_push.yml +7 -7
- {tx_engine-0.11.2 → tx_engine-0.11.3}/Cargo.lock +26 -25
- {tx_engine-0.11.2 → tx_engine-0.11.3}/Cargo.toml +5 -2
- {tx_engine-0.11.2 → tx_engine-0.11.3}/PKG-INFO +1 -1
- {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/Releases.md +1 -0
- tx_engine-0.11.3/python/src/tests/test_woc_balance.py +63 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_woc_interface.py +18 -1
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/interface/woc.py +79 -21
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/interface/woc_interface.py +8 -6
- tx_engine-0.11.3/src/interface/woc_interface.rs +633 -0
- tx_engine-0.11.2/src/interface/woc_interface.rs +0 -361
- {tx_engine-0.11.2 → tx_engine-0.11.3}/.github/dependabot.yml +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/.gitignore +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/LICENSE +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/LICENSE-rust-sv +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/README-chain-gang.md +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/README-pypi.md +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/README.md +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/BIP-32-Python.md +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/BIP-32.md +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/Chronicle-Python.md +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/Chronicle.md +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/Development.md +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/Python-API.md +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/README-chain-gang.md +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/README.md +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/Regtest.md +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/Requirements +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/diagrams/keys.png +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/diagrams/keys.puml +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/diagrams/overview.png +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/diagrams/overview.puml +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/diagrams/python_classes.png +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/diagrams/python_classes.puml +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/pyproject.toml +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/README.md +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/lint.sh +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/requirements.txt +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/README.md +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_bit_twiddling.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_bsv.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_chronicle_context.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_chronicle_opcodes.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_chronicle_tx_validate.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_debug.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_ec.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_fed.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_hd_wallet.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_if.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_interface.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_op.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_parse.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_rpc_interface.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_script.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_sighash.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_sign.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_tx.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_verify_script.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_wallet.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_wif_from_pw_nonce.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_woc.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/__init__.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/engine/__init__.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/engine/context.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/engine/cryptography_utils.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/engine/engine_types.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/engine/op_code_names.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/engine/op_codes.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/engine/util.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/interface/__init__.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/interface/blockchain_interface.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/interface/interface_factory.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/interface/mock_interface.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/interface/rpc_interface.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/interface/verify_script.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/tx/__init__.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/tx/sighash.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/python/tests.sh +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/address/mod.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/chronicle.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/interface/blockchain_interface.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/interface/mod.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/interface/rpc_interface.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/interface/test_interface.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/interface/uaas_interface.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/lib.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/addr.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/authch.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/block.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/block_header.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/block_locator.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/blocktxn.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/cmpctblock.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/createstrm.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/fee_filter.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/filter_add.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/filter_load.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/getblocktxn.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/headers.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/inv.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/inv_vect.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/merkle_block.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/message.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/message_header.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/mod.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/node_addr.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/node_addr_ex.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/out_point.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/ping.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/protoconf.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/reject.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/send_cmpct.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/streamack.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/tx.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/tx_chronicle_validate.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/tx_in.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/tx_out.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/version.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/network/mod.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/network/network.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/network/seed_iter.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/peer/atomic_reader.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/peer/mod.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/peer/peer.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/python/mod.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/python/py_hd_wallet.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/python/py_script.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/python/py_stack.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/python/py_tx.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/python/py_wallet.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/script/checker.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/script/format.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/script/interpreter/eval.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/script/interpreter/mod.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/script/interpreter/multisig.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/script/interpreter/push.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/script/interpreter/rules.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/script/interpreter/script_code.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/script/interpreter/tests.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/script/mod.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/script/op_codes.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/script/stack.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/test_util.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/transaction/mod.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/transaction/p2pkh.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/transaction/sighash.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/bits.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/bloom_filter.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/bounded_read.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/errors.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/future.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/hash160.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/hash256.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/latch.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/mod.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/rx.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/serdes.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/sha1.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/sha256.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/var_int.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/base58_checksum.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/extended_key.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/hd_wallet.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/hd_watch_wallet.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/mnemonic.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/mod.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/wallet.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/wordlists/chinese_simplified.txt +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/wordlists/chinese_traditional.txt +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/wordlists/english.txt +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/wordlists/french.txt +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/wordlists/italian.txt +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/wordlists/japanese.txt +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/wordlists/korean.txt +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/wordlists/spanish.txt +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/tests/hostile_lengths.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/tests/regtest_p2p.rs +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/tools/README.md +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/tools/dbg.py +0 -0
- {tx_engine-0.11.2 → tx_engine-0.11.3}/tools/generate_key.py +0 -0
|
@@ -47,8 +47,8 @@ jobs:
|
|
|
47
47
|
#- runner: ubuntu-latest
|
|
48
48
|
# target: ppc64le
|
|
49
49
|
steps:
|
|
50
|
-
- uses: actions/checkout@
|
|
51
|
-
- uses: actions/setup-python@
|
|
50
|
+
- uses: actions/checkout@v7
|
|
51
|
+
- uses: actions/setup-python@v7
|
|
52
52
|
with:
|
|
53
53
|
python-version: '3.11'
|
|
54
54
|
- name: Build wheels
|
|
@@ -59,7 +59,7 @@ jobs:
|
|
|
59
59
|
sccache: 'true'
|
|
60
60
|
manylinux: auto
|
|
61
61
|
- name: Upload wheels
|
|
62
|
-
uses: actions/upload-artifact@
|
|
62
|
+
uses: actions/upload-artifact@v7
|
|
63
63
|
with:
|
|
64
64
|
name: wheels-linux-${{ matrix.platform.target }}
|
|
65
65
|
path: dist
|
|
@@ -75,8 +75,8 @@ jobs:
|
|
|
75
75
|
- runner: windows-latest
|
|
76
76
|
target: x86
|
|
77
77
|
steps:
|
|
78
|
-
- uses: actions/checkout@
|
|
79
|
-
- uses: actions/setup-python@
|
|
78
|
+
- uses: actions/checkout@v7
|
|
79
|
+
- uses: actions/setup-python@v7
|
|
80
80
|
with:
|
|
81
81
|
python-version: '3.11'
|
|
82
82
|
architecture: ${{ matrix.platform.target }}
|
|
@@ -87,7 +87,7 @@ jobs:
|
|
|
87
87
|
args: --release --out dist --interpreter 3.11
|
|
88
88
|
sccache: 'true'
|
|
89
89
|
- name: Upload wheels
|
|
90
|
-
uses: actions/upload-artifact@
|
|
90
|
+
uses: actions/upload-artifact@v7
|
|
91
91
|
with:
|
|
92
92
|
name: wheels-windows-${{ matrix.platform.target }}
|
|
93
93
|
path: dist
|
|
@@ -103,8 +103,8 @@ jobs:
|
|
|
103
103
|
- runner: macos-14
|
|
104
104
|
target: aarch64
|
|
105
105
|
steps:
|
|
106
|
-
- uses: actions/checkout@
|
|
107
|
-
- uses: actions/setup-python@
|
|
106
|
+
- uses: actions/checkout@v7
|
|
107
|
+
- uses: actions/setup-python@v7
|
|
108
108
|
with:
|
|
109
109
|
python-version: '3.11'
|
|
110
110
|
- name: Build wheels
|
|
@@ -114,7 +114,7 @@ jobs:
|
|
|
114
114
|
args: --release --out dist --interpreter 3.11
|
|
115
115
|
sccache: 'true'
|
|
116
116
|
- name: Upload wheels
|
|
117
|
-
uses: actions/upload-artifact@
|
|
117
|
+
uses: actions/upload-artifact@v7
|
|
118
118
|
with:
|
|
119
119
|
name: wheels-macos-${{ matrix.platform.target }}
|
|
120
120
|
path: dist
|
|
@@ -123,14 +123,14 @@ jobs:
|
|
|
123
123
|
runs-on: ubuntu-latest
|
|
124
124
|
if: "startsWith(github.ref, 'refs/tags/')"
|
|
125
125
|
steps:
|
|
126
|
-
- uses: actions/checkout@
|
|
126
|
+
- uses: actions/checkout@v7
|
|
127
127
|
- name: Build sdist
|
|
128
128
|
uses: PyO3/maturin-action@v1
|
|
129
129
|
with:
|
|
130
130
|
command: sdist
|
|
131
131
|
args: --out dist
|
|
132
132
|
- name: Upload sdist
|
|
133
|
-
uses: actions/upload-artifact@
|
|
133
|
+
uses: actions/upload-artifact@v7
|
|
134
134
|
with:
|
|
135
135
|
name: wheels-sdist
|
|
136
136
|
path: dist
|
|
@@ -141,7 +141,7 @@ jobs:
|
|
|
141
141
|
if: "startsWith(github.ref, 'refs/tags/')"
|
|
142
142
|
needs: [linux, windows, macos, sdist]
|
|
143
143
|
steps:
|
|
144
|
-
- uses: actions/download-artifact@
|
|
144
|
+
- uses: actions/download-artifact@v8
|
|
145
145
|
- name: Publish to PyPI
|
|
146
146
|
uses: PyO3/maturin-action@v1
|
|
147
147
|
env:
|
|
@@ -155,7 +155,7 @@ jobs:
|
|
|
155
155
|
runs-on: ubuntu-latest
|
|
156
156
|
if: "startsWith(github.ref, 'refs/tags/')"
|
|
157
157
|
steps:
|
|
158
|
-
- uses: actions/checkout@
|
|
158
|
+
- uses: actions/checkout@v7
|
|
159
159
|
- name: Install Rust
|
|
160
160
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
|
161
161
|
with:
|
|
@@ -13,9 +13,9 @@ jobs:
|
|
|
13
13
|
runs-on: ubuntu-latest
|
|
14
14
|
steps:
|
|
15
15
|
- name: Checkout branch code
|
|
16
|
-
uses: actions/checkout@
|
|
16
|
+
uses: actions/checkout@v7
|
|
17
17
|
- name: Install Python
|
|
18
|
-
uses: actions/setup-python@
|
|
18
|
+
uses: actions/setup-python@v7
|
|
19
19
|
with:
|
|
20
20
|
python-version: '3.12'
|
|
21
21
|
cache: 'pip'
|
|
@@ -31,7 +31,7 @@ jobs:
|
|
|
31
31
|
runs-on: ubuntu-latest
|
|
32
32
|
steps:
|
|
33
33
|
- name: Checkout branch code
|
|
34
|
-
uses: actions/checkout@
|
|
34
|
+
uses: actions/checkout@v7
|
|
35
35
|
- name: Install Rust
|
|
36
36
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
|
37
37
|
with:
|
|
@@ -45,7 +45,7 @@ jobs:
|
|
|
45
45
|
runs-on: ubuntu-latest
|
|
46
46
|
steps:
|
|
47
47
|
- name: Checkout branch code
|
|
48
|
-
uses: actions/checkout@
|
|
48
|
+
uses: actions/checkout@v7
|
|
49
49
|
- name: Install Rust
|
|
50
50
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
|
51
51
|
with:
|
|
@@ -75,7 +75,7 @@ jobs:
|
|
|
75
75
|
args: "--no-default-features"
|
|
76
76
|
steps:
|
|
77
77
|
- name: Checkout branch code
|
|
78
|
-
uses: actions/checkout@
|
|
78
|
+
uses: actions/checkout@v7
|
|
79
79
|
- name: Install Rust
|
|
80
80
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
|
81
81
|
with:
|
|
@@ -93,9 +93,9 @@ jobs:
|
|
|
93
93
|
python-version: ['3.11', '3.12', '3.13', '3.14']
|
|
94
94
|
steps:
|
|
95
95
|
- name: Checkout branch code
|
|
96
|
-
uses: actions/checkout@
|
|
96
|
+
uses: actions/checkout@v7
|
|
97
97
|
- name: Install Python
|
|
98
|
-
uses: actions/setup-python@
|
|
98
|
+
uses: actions/setup-python@v7
|
|
99
99
|
with:
|
|
100
100
|
python-version: ${{ matrix.python-version }}
|
|
101
101
|
cache: 'pip'
|
|
@@ -30,7 +30,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
|
|
|
30
30
|
dependencies = [
|
|
31
31
|
"proc-macro2",
|
|
32
32
|
"quote",
|
|
33
|
-
"syn 3.0.
|
|
33
|
+
"syn 3.0.6",
|
|
34
34
|
]
|
|
35
35
|
|
|
36
36
|
[[package]]
|
|
@@ -136,9 +136,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
|
|
136
136
|
|
|
137
137
|
[[package]]
|
|
138
138
|
name = "cc"
|
|
139
|
-
version = "1.4.
|
|
139
|
+
version = "1.4.7"
|
|
140
140
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
141
|
-
checksum = "
|
|
141
|
+
checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313"
|
|
142
142
|
dependencies = [
|
|
143
143
|
"find-msvc-tools",
|
|
144
144
|
"jobserver",
|
|
@@ -148,9 +148,9 @@ dependencies = [
|
|
|
148
148
|
|
|
149
149
|
[[package]]
|
|
150
150
|
name = "cfg-if"
|
|
151
|
-
version = "1.0.
|
|
151
|
+
version = "1.0.5"
|
|
152
152
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
153
|
-
checksum = "
|
|
153
|
+
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
|
154
154
|
|
|
155
155
|
[[package]]
|
|
156
156
|
name = "cfg_aliases"
|
|
@@ -171,7 +171,7 @@ dependencies = [
|
|
|
171
171
|
|
|
172
172
|
[[package]]
|
|
173
173
|
name = "chain-gang"
|
|
174
|
-
version = "0.11.
|
|
174
|
+
version = "0.11.3"
|
|
175
175
|
dependencies = [
|
|
176
176
|
"async-lock",
|
|
177
177
|
"async-trait",
|
|
@@ -197,6 +197,7 @@ dependencies = [
|
|
|
197
197
|
"sha1",
|
|
198
198
|
"sha2",
|
|
199
199
|
"thiserror",
|
|
200
|
+
"tokio",
|
|
200
201
|
"url",
|
|
201
202
|
]
|
|
202
203
|
|
|
@@ -345,7 +346,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
|
|
|
345
346
|
dependencies = [
|
|
346
347
|
"proc-macro2",
|
|
347
348
|
"quote",
|
|
348
|
-
"syn 3.0.
|
|
349
|
+
"syn 3.0.6",
|
|
349
350
|
]
|
|
350
351
|
|
|
351
352
|
[[package]]
|
|
@@ -454,9 +455,9 @@ dependencies = [
|
|
|
454
455
|
|
|
455
456
|
[[package]]
|
|
456
457
|
name = "find-msvc-tools"
|
|
457
|
-
version = "0.1.
|
|
458
|
+
version = "0.1.13"
|
|
458
459
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
459
|
-
checksum = "
|
|
460
|
+
checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b"
|
|
460
461
|
|
|
461
462
|
[[package]]
|
|
462
463
|
name = "fnv"
|
|
@@ -675,9 +676,9 @@ dependencies = [
|
|
|
675
676
|
|
|
676
677
|
[[package]]
|
|
677
678
|
name = "hyper-rustls"
|
|
678
|
-
version = "0.27.
|
|
679
|
+
version = "0.27.10"
|
|
679
680
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
680
|
-
checksum = "
|
|
681
|
+
checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53"
|
|
681
682
|
dependencies = [
|
|
682
683
|
"http",
|
|
683
684
|
"hyper",
|
|
@@ -995,7 +996,7 @@ dependencies = [
|
|
|
995
996
|
"proc-macro2",
|
|
996
997
|
"quote",
|
|
997
998
|
"rustversion",
|
|
998
|
-
"syn 3.0.
|
|
999
|
+
"syn 3.0.6",
|
|
999
1000
|
]
|
|
1000
1001
|
|
|
1001
1002
|
[[package]]
|
|
@@ -1277,9 +1278,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
|
|
1277
1278
|
|
|
1278
1279
|
[[package]]
|
|
1279
1280
|
name = "rand"
|
|
1280
|
-
version = "0.10.
|
|
1281
|
+
version = "0.10.3"
|
|
1281
1282
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1282
|
-
checksum = "
|
|
1283
|
+
checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af"
|
|
1283
1284
|
dependencies = [
|
|
1284
1285
|
"chacha20",
|
|
1285
1286
|
"getrandom 0.4.3",
|
|
@@ -1593,7 +1594,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
|
|
1593
1594
|
dependencies = [
|
|
1594
1595
|
"proc-macro2",
|
|
1595
1596
|
"quote",
|
|
1596
|
-
"syn 3.0.
|
|
1597
|
+
"syn 3.0.6",
|
|
1597
1598
|
]
|
|
1598
1599
|
|
|
1599
1600
|
[[package]]
|
|
@@ -1730,9 +1731,9 @@ dependencies = [
|
|
|
1730
1731
|
|
|
1731
1732
|
[[package]]
|
|
1732
1733
|
name = "syn"
|
|
1733
|
-
version = "3.0.
|
|
1734
|
+
version = "3.0.6"
|
|
1734
1735
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1735
|
-
checksum = "
|
|
1736
|
+
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
|
1736
1737
|
dependencies = [
|
|
1737
1738
|
"proc-macro2",
|
|
1738
1739
|
"quote",
|
|
@@ -1756,7 +1757,7 @@ checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"
|
|
|
1756
1757
|
dependencies = [
|
|
1757
1758
|
"proc-macro2",
|
|
1758
1759
|
"quote",
|
|
1759
|
-
"syn 3.0.
|
|
1760
|
+
"syn 3.0.6",
|
|
1760
1761
|
]
|
|
1761
1762
|
|
|
1762
1763
|
[[package]]
|
|
@@ -1803,7 +1804,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
|
|
1803
1804
|
dependencies = [
|
|
1804
1805
|
"proc-macro2",
|
|
1805
1806
|
"quote",
|
|
1806
|
-
"syn 3.0.
|
|
1807
|
+
"syn 3.0.6",
|
|
1807
1808
|
]
|
|
1808
1809
|
|
|
1809
1810
|
[[package]]
|
|
@@ -1938,9 +1939,9 @@ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
|
|
1938
1939
|
|
|
1939
1940
|
[[package]]
|
|
1940
1941
|
name = "unicode-ident"
|
|
1941
|
-
version = "1.0.
|
|
1942
|
+
version = "1.0.26"
|
|
1942
1943
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1943
|
-
checksum = "
|
|
1944
|
+
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
|
1944
1945
|
|
|
1945
1946
|
[[package]]
|
|
1946
1947
|
name = "untrusted"
|
|
@@ -2033,7 +2034,7 @@ dependencies = [
|
|
|
2033
2034
|
"bumpalo",
|
|
2034
2035
|
"proc-macro2",
|
|
2035
2036
|
"quote",
|
|
2036
|
-
"syn 3.0.
|
|
2037
|
+
"syn 3.0.6",
|
|
2037
2038
|
"wasm-bindgen-shared",
|
|
2038
2039
|
]
|
|
2039
2040
|
|
|
@@ -2238,7 +2239,7 @@ checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78"
|
|
|
2238
2239
|
dependencies = [
|
|
2239
2240
|
"proc-macro2",
|
|
2240
2241
|
"quote",
|
|
2241
|
-
"syn 3.0.
|
|
2242
|
+
"syn 3.0.6",
|
|
2242
2243
|
"synstructure",
|
|
2243
2244
|
]
|
|
2244
2245
|
|
|
@@ -2259,7 +2260,7 @@ checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a"
|
|
|
2259
2260
|
dependencies = [
|
|
2260
2261
|
"proc-macro2",
|
|
2261
2262
|
"quote",
|
|
2262
|
-
"syn 3.0.
|
|
2263
|
+
"syn 3.0.6",
|
|
2263
2264
|
"synstructure",
|
|
2264
2265
|
]
|
|
2265
2266
|
|
|
@@ -2299,7 +2300,7 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
|
|
|
2299
2300
|
dependencies = [
|
|
2300
2301
|
"proc-macro2",
|
|
2301
2302
|
"quote",
|
|
2302
|
-
"syn 3.0.
|
|
2303
|
+
"syn 3.0.6",
|
|
2303
2304
|
]
|
|
2304
2305
|
|
|
2305
2306
|
[[package]]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "chain-gang"
|
|
3
|
-
version = "0.11.
|
|
3
|
+
version = "0.11.3"
|
|
4
4
|
description = "This is a library that enables monitoring of multiple blockchains (BTC, BCH, BSV)."
|
|
5
5
|
repository = "https://github.com/nchain-innovation/chain-gang"
|
|
6
6
|
homepage = "https://github.com/nchain-innovation/chain-gang"
|
|
@@ -44,6 +44,9 @@ serde_json = { version = "1.0.151" }
|
|
|
44
44
|
reqwest = { version = "0.13.4", features = ["json"], optional = true }
|
|
45
45
|
async-lock = { version = "3.4.2", optional = true }
|
|
46
46
|
async-trait = { version = "0.1.91", optional = true }
|
|
47
|
+
# Already in the tree via reqwest; the "time" feature is for the backoff
|
|
48
|
+
# between paginated WhatsOnChain requests
|
|
49
|
+
tokio = { version = "1", features = ["time"], optional = true }
|
|
47
50
|
|
|
48
51
|
# For python feature
|
|
49
52
|
# abi3-py311 builds against the stable ABI: one wheel (cp311-abi3) works on
|
|
@@ -64,5 +67,5 @@ lto = true # Enable link-time optimizations to shrink binary
|
|
|
64
67
|
# serde and serde_json are unconditional dependencies, so there is nothing
|
|
65
68
|
# left for the default feature to switch on
|
|
66
69
|
default = []
|
|
67
|
-
interface = ["dep:reqwest", "dep:async-lock", "dep:async-trait"]
|
|
70
|
+
interface = ["dep:reqwest", "dep:async-lock", "dep:async-trait", "dep:tokio"]
|
|
68
71
|
python = ["dep:pyo3"]
|
|
@@ -59,3 +59,4 @@
|
|
|
59
59
|
* v0.11.0 - **Breaking.** Regtest support: `Network::BSV_Regtest` and a JSON-RPC `RpcInterface` for talking to a local node, verified against a bitcoin-sv regtest node. `Peer::id` is now `PeerId` rather than `snowflake::ProcessUniqueId`; `generate_wif` takes a `Network` instead of its name and returns `Result`, where an unrecognised name used to yield a mainnet key; `decode_base58_checksum`, `ExtendedKey::decode` and `WocInterface` return errors where they used to panic; unconfirmed UTXOs report `UNCONFIRMED_HEIGHT` (-1) through every interface, WhatsOnChain's own `height: 0` being translated on the way in; `Block::validate` enforces the Chronicle activation height it already had; `ChainGangError` no longer discards the message of `ResponseError`, `JSONParseError` and `InvalidOperation`. `serde` and `serde_json` are ordinary dependencies, so `--no-default-features` compiles. Dropped `typenum`, `lazy_static` and `snowflake`; moved `async-mutex` to `async-lock` and `base58` to `bs58`. CI gained clippy, a feature matrix and Python lint gates
|
|
60
60
|
* v0.11.1 - `RpcInterface::import_address` asks the node to watch an address, opt-in because whether to touch the node's wallet is the caller's decision; a JSON-RPC method answering `"result": null`, as `importaddress` does, is now success rather than "returned neither a result nor an error"; documented that balance and UTXO queries see only what the node's wallet tracks, so an unwatched address reads as zero with no error, in both the Rust and Python clients; added [Regtest.md](Regtest.md), a guide to running against a private chain; cleared the rustdoc warnings
|
|
61
61
|
* v0.11.2 - Two robustness fixes. Additive API only, so cargo treats this as a compatible upgrade: `util::read_exact_vec`, `util::bounded_capacity`, `util::MAX_PREALLOC_ELEMENTS` and `script::stack::pop_bigint_checked_minimal` are new, and nothing existing changes shape. Wire-format reads no longer size an allocation from a sender-controlled length prefix: 17 sites across `tx_in`/`tx_out`, `tx`, `block`, `merkle_block`, `filter_add`/`filter_load`, `protoconf`/`createstrm`, `version` and `reject` now grow a buffer as bytes arrive via `util::read_exact_vec`, or cap a speculative reserve with `util::bounded_capacity`, so a 27-byte transaction declaring a 2^48-byte script is an error rather than an allocation-failure `SIGABRT` that a consumer cannot catch. Script evaluation applies minimal *number* encoding only where a stack item is decoded as a number, not to every push: a 33-byte public key or a 20-byte hash ending `0x00` or `0x80` is ordinary data, and rejecting it as `Non-minimal push data` refused roughly 1 in 128 standard P2PKH spends. Minimal-*push* (BIP-62) is unchanged, and minimal-number enforcement now also covers the bigint arithmetic operands it previously missed
|
|
62
|
+
* v0.11.3 - WhatsOnChain endpoint fixes. No API change: `Balance` and `UtxoEntry` keep their fields, and the new types are internal. `get_utxo` reads `/address/{addr}/unspent/all` and follows every page, where it previously read the undocumented `/unspent` and stopped at 1000 entries with no way to ask for the rest, so a busy address came back silently truncated and a caller could not tell 1000 UTXOs from 50,000; on `1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa` that is 20960 entries rather than 1000, the old result being a strict subset of the new one. Unconfirmed outputs are now recognised by the entry's `status` field, falling back to `height == 0` as before. `get_balance` reads `/confirmed/balance` and `/unconfirmed/balance`, the endpoints that replaced the undocumented combined `/balance`, reproducing its figures to the satoshi. Both endpoints report failures in the body with HTTP 200, so an error is no longer read as an empty UTXO set or a zero balance. **Paging and the split balance turn one request into several, and WhatsOnChain rate limits**, so requests now back off on 429/502/503/504, matching what the Python client already did; `tokio` joins the `interface` feature for its timer, having already been present through `reqwest`. The parallel Python client is changed to match. CI actions moved off the deprecated Node 20 runtime
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
""" Test of the WoC balance split across confirmed/unconfirmed endpoints
|
|
2
|
+
|
|
3
|
+
WhatsOnChain replaced the combined /address/{addr}/balance with separate
|
|
4
|
+
confirmed and unconfirmed endpoints. Covers the combining, which needs no
|
|
5
|
+
network.
|
|
6
|
+
"""
|
|
7
|
+
import unittest
|
|
8
|
+
from unittest.mock import patch
|
|
9
|
+
|
|
10
|
+
from tx_engine.interface import woc
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class WoCBalanceTest(unittest.TestCase):
|
|
14
|
+
""" Test of the WoC balance combining """
|
|
15
|
+
|
|
16
|
+
def test_the_two_halves_combine(self):
|
|
17
|
+
# Shapes as observed against mainnet
|
|
18
|
+
responses = [
|
|
19
|
+
{"address": "1A1z", "script": "8b01df4e", "confirmed": 7297358945,
|
|
20
|
+
"error": "", "associatedScripts": [{"script": "740485f3", "type": "pubkey"}]},
|
|
21
|
+
{"address": "1A1z", "script": "8b01df4e", "unconfirmed": 42, "error": ""},
|
|
22
|
+
]
|
|
23
|
+
with patch.object(woc, "get_response", side_effect=responses):
|
|
24
|
+
self.assertEqual(
|
|
25
|
+
woc.get_balance("1A1z", testnet=False),
|
|
26
|
+
{"confirmed": 7297358945, "unconfirmed": 42},
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def test_a_body_error_is_not_mistaken_for_a_zero_balance(self):
|
|
30
|
+
# Reported with HTTP 200, so the body is the only signal
|
|
31
|
+
with patch.object(
|
|
32
|
+
woc, "get_response",
|
|
33
|
+
side_effect=[{"confirmed": 0, "error": "invalid address"}],
|
|
34
|
+
):
|
|
35
|
+
self.assertIsNone(woc.get_balance("nonsense", testnet=False))
|
|
36
|
+
|
|
37
|
+
def test_a_failed_request_gives_none(self):
|
|
38
|
+
with patch.object(woc, "get_response", side_effect=[None]):
|
|
39
|
+
self.assertIsNone(woc.get_balance("1A1z", testnet=False))
|
|
40
|
+
|
|
41
|
+
def test_a_failed_second_request_gives_none(self):
|
|
42
|
+
# The unconfirmed half failing must not report a confirmed-only balance
|
|
43
|
+
with patch.object(
|
|
44
|
+
woc, "get_response",
|
|
45
|
+
side_effect=[{"confirmed": 100, "error": ""}, None],
|
|
46
|
+
):
|
|
47
|
+
self.assertIsNone(woc.get_balance("1A1z", testnet=False))
|
|
48
|
+
|
|
49
|
+
def test_a_negative_unconfirmed_balance_survives(self):
|
|
50
|
+
# Spending a confirmed UTXO from the mempool makes this negative
|
|
51
|
+
with patch.object(
|
|
52
|
+
woc, "get_response",
|
|
53
|
+
side_effect=[{"confirmed": 100, "error": ""},
|
|
54
|
+
{"unconfirmed": -5000, "error": ""}],
|
|
55
|
+
):
|
|
56
|
+
self.assertEqual(
|
|
57
|
+
woc.get_balance("1A1z", testnet=False),
|
|
58
|
+
{"confirmed": 100, "unconfirmed": -5000},
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
unittest.main()
|
|
@@ -14,7 +14,7 @@ class WoCHeightTest(unittest.TestCase):
|
|
|
14
14
|
""" Test of the WoC unconfirmed height normalisation """
|
|
15
15
|
|
|
16
16
|
def test_height_zero_becomes_the_unconfirmed_sentinel(self):
|
|
17
|
-
# Trimmed from a real /address/{addr}/unspent response. Transaction
|
|
17
|
+
# Trimmed from a real /address/{addr}/unspent/all response. Transaction
|
|
18
18
|
# fa7f15f8..d860 was in WhatsOnChain's own mempool/raw list, with no
|
|
19
19
|
# blockhash, blockheight or confirmations.
|
|
20
20
|
utxo: List[Dict[str, Any]] = [
|
|
@@ -28,6 +28,23 @@ class WoCHeightTest(unittest.TestCase):
|
|
|
28
28
|
self.assertEqual(utxo[0]["height"], 964754)
|
|
29
29
|
self.assertEqual(utxo[1]["value"], 2954693)
|
|
30
30
|
|
|
31
|
+
def test_status_marks_an_unconfirmed_entry_even_at_a_real_height(self):
|
|
32
|
+
# status is the documented signal on /unspent/all; height is the fallback
|
|
33
|
+
utxo: List[Dict[str, Any]] = [
|
|
34
|
+
{"height": 964754, "tx_pos": 0, "tx_hash": "aa", "value": 1,
|
|
35
|
+
"isSpentInMempoolTx": False, "status": "unconfirmed"},
|
|
36
|
+
]
|
|
37
|
+
_normalise_unconfirmed(utxo)
|
|
38
|
+
self.assertEqual(utxo[0]["height"], UNCONFIRMED_HEIGHT)
|
|
39
|
+
|
|
40
|
+
def test_a_confirmed_status_keeps_its_height(self):
|
|
41
|
+
utxo: List[Dict[str, Any]] = [
|
|
42
|
+
{"height": 964754, "tx_pos": 0, "tx_hash": "aa", "value": 1,
|
|
43
|
+
"isSpentInMempoolTx": False, "status": "confirmed"},
|
|
44
|
+
]
|
|
45
|
+
_normalise_unconfirmed(utxo)
|
|
46
|
+
self.assertEqual(utxo[0]["height"], 964754)
|
|
47
|
+
|
|
31
48
|
def test_is_idempotent(self):
|
|
32
49
|
utxo: List[Dict[str, Any]] = [
|
|
33
50
|
{"height": UNCONFIRMED_HEIGHT, "tx_pos": 0, "tx_hash": "aa", "value": 1}
|
|
@@ -23,15 +23,16 @@ def get_url(testnet: bool = True) -> str:
|
|
|
23
23
|
return "https://api.whatsonchain.com/v1/bsv/main"
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
def get_response(url: str, max_retries: int = 5):
|
|
26
|
+
def get_response(url: str, max_retries: int = 5, params: Optional[Dict] = None):
|
|
27
27
|
"""This is the guts of all the WoC requests.
|
|
28
28
|
|
|
29
29
|
Retries on connection errors and transient HTTP responses (429/502/503/504).
|
|
30
|
+
`params` are appended as a query string, encoded by requests.
|
|
30
31
|
"""
|
|
31
32
|
transient_status = {429, 502, 503, 504}
|
|
32
33
|
for attempt in range(max_retries):
|
|
33
34
|
try:
|
|
34
|
-
response = requests.get(url, timeout=30)
|
|
35
|
+
response = requests.get(url, timeout=30, params=params)
|
|
35
36
|
except (ConnectionError, requests.Timeout) as e:
|
|
36
37
|
LOGGER.warning(f"WoC request error for {url}: {e}")
|
|
37
38
|
if attempt + 1 >= max_retries:
|
|
@@ -58,27 +59,56 @@ def get_response(url: str, max_retries: int = 5):
|
|
|
58
59
|
return None
|
|
59
60
|
|
|
60
61
|
|
|
62
|
+
#: Upper bound on pages fetched for one address. Each page is up to 1000
|
|
63
|
+
#: entries, so this allows 1M UTXOs. It exists only so that a server that kept
|
|
64
|
+
#: handing back a next-page token could not spin forever; reaching it raises
|
|
65
|
+
#: rather than truncating, which is the whole point of moving off /unspent.
|
|
66
|
+
MAX_UTXO_PAGES = 1000
|
|
67
|
+
|
|
68
|
+
|
|
61
69
|
def get_unspent_transactions(address: str, testnet: bool = True):
|
|
62
|
-
"""Return the unspent
|
|
63
|
-
|
|
70
|
+
"""Return the unspent transactions associated with this address
|
|
71
|
+
|
|
72
|
+
Reads /unspent/all, which covers both confirmed and unconfirmed outputs and
|
|
73
|
+
paginates at 1000 entries. Every page is followed, so the returned list is
|
|
74
|
+
complete; the superseded /unspent stopped at 1000 with no way to ask for
|
|
75
|
+
the rest, so a busy address came back silently truncated.
|
|
76
|
+
|
|
77
|
+
Returns a list of entries, as before. None if a request fails.
|
|
78
|
+
"""
|
|
79
|
+
base = f"{get_url(testnet)}/address/{address}/unspent/all"
|
|
80
|
+
entries: list = []
|
|
81
|
+
token = ""
|
|
82
|
+
|
|
83
|
+
for _ in range(MAX_UTXO_PAGES):
|
|
84
|
+
# The token is opaque and server-supplied, so it is passed as a
|
|
85
|
+
# parameter rather than interpolated into the path
|
|
86
|
+
page = get_response(base, params={"token": token} if token else None)
|
|
87
|
+
if page is None:
|
|
88
|
+
return None
|
|
89
|
+
error = page.get("error")
|
|
90
|
+
if error:
|
|
91
|
+
# Reported in the body with a 200, so it would otherwise read as an
|
|
92
|
+
# empty UTXO set
|
|
93
|
+
LOGGER.warning(f"WhatsOnChain error = {error}")
|
|
94
|
+
return None
|
|
95
|
+
entries.extend(page.get("result") or [])
|
|
96
|
+
|
|
97
|
+
next_token = page.get("nextPageToken") or ""
|
|
98
|
+
if not next_token or next_token == token:
|
|
99
|
+
return entries
|
|
100
|
+
token = next_token
|
|
101
|
+
|
|
102
|
+
raise ValueError(f"address has more than {MAX_UTXO_PAGES} pages of UTXOs")
|
|
64
103
|
|
|
65
104
|
|
|
66
105
|
def get_last_unspent(address: str, testnet: bool = True):
|
|
67
|
-
"""Return the unspent
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if response.status_code == 200:
|
|
74
|
-
data = response.json()
|
|
75
|
-
LOGGER.debug(f"data = {data}")
|
|
76
|
-
tx_hash = data[-1]["tx_hash"]
|
|
77
|
-
tx_pos = data[-1]["tx_pos"]
|
|
78
|
-
value = data[-1]["value"]
|
|
79
|
-
else:
|
|
80
|
-
LOGGER.info(f"response = {response}")
|
|
81
|
-
return (tx_hash, tx_pos, value)
|
|
106
|
+
"""Return the last unspent transaction associated with this address"""
|
|
107
|
+
data = get_unspent_transactions(address, testnet=testnet)
|
|
108
|
+
if not data:
|
|
109
|
+
return (None, 0, 0)
|
|
110
|
+
LOGGER.debug(f"data = {data}")
|
|
111
|
+
return (data[-1]["tx_hash"], data[-1]["tx_pos"], data[-1]["value"])
|
|
82
112
|
|
|
83
113
|
|
|
84
114
|
def get_transaction(tx_id: str, testnet: bool = True):
|
|
@@ -109,8 +139,36 @@ def get_history(address: str, testnet: bool = True):
|
|
|
109
139
|
|
|
110
140
|
|
|
111
141
|
def get_balance(address: str, testnet: bool = True):
|
|
112
|
-
"""Return the balance associated with this address
|
|
113
|
-
|
|
142
|
+
"""Return the balance associated with this address
|
|
143
|
+
|
|
144
|
+
Reads /confirmed/balance and /unconfirmed/balance, the endpoints that
|
|
145
|
+
replaced the combined /balance. That costs two requests where there was
|
|
146
|
+
one, so the two halves are read a moment apart; an address being spent to
|
|
147
|
+
between them could report a confirmed figure from just before a block and
|
|
148
|
+
an unconfirmed one from just after. The combined endpoint is undocumented,
|
|
149
|
+
which is the trade being made.
|
|
150
|
+
|
|
151
|
+
Returns {"confirmed": int, "unconfirmed": int} as before, or None if
|
|
152
|
+
either request fails.
|
|
153
|
+
"""
|
|
154
|
+
base = f"{get_url(testnet)}/address/{address}"
|
|
155
|
+
|
|
156
|
+
confirmed = get_response(f"{base}/confirmed/balance")
|
|
157
|
+
if confirmed is None or confirmed.get("error"):
|
|
158
|
+
if confirmed is not None:
|
|
159
|
+
LOGGER.warning(f"WhatsOnChain error = {confirmed['error']}")
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
unconfirmed = get_response(f"{base}/unconfirmed/balance")
|
|
163
|
+
if unconfirmed is None or unconfirmed.get("error"):
|
|
164
|
+
if unconfirmed is not None:
|
|
165
|
+
LOGGER.warning(f"WhatsOnChain error = {unconfirmed['error']}")
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
"confirmed": confirmed.get("confirmed", 0),
|
|
170
|
+
"unconfirmed": unconfirmed.get("unconfirmed", 0),
|
|
171
|
+
}
|
|
114
172
|
|
|
115
173
|
|
|
116
174
|
def get_chain_info(testnet: bool = True):
|
|
@@ -20,14 +20,16 @@ LOGGER = logging.getLogger(__name__)
|
|
|
20
20
|
def _normalise_unconfirmed(utxo):
|
|
21
21
|
"""Rewrite WhatsOnChain's unconfirmed marker to this package's
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
A negative height is what means unconfirmed here and in the Rust crate.
|
|
24
|
+
WhatsOnChain signals it two ways: `status: "unconfirmed"` on /unspent/all,
|
|
25
|
+
and `height: 0`. Both are honoured, the status first, so the value is
|
|
26
|
+
translated on the way in rather than left for every caller to
|
|
27
|
+
special-case. A height of 0 cannot mean the genesis block: the genesis
|
|
28
|
+
coinbase is unspendable, so it never appears in an unspent set.
|
|
28
29
|
"""
|
|
29
30
|
for entry in utxo:
|
|
30
|
-
|
|
31
|
+
status = str(entry.get("status", "")).lower()
|
|
32
|
+
if status == "unconfirmed" or entry.get("height") == 0:
|
|
31
33
|
entry["height"] = UNCONFIRMED_HEIGHT
|
|
32
34
|
return utxo
|
|
33
35
|
|