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.
Files changed (181) hide show
  1. {tx_engine-0.11.2 → tx_engine-0.11.3}/.github/workflows/CI.yml +13 -13
  2. {tx_engine-0.11.2 → tx_engine-0.11.3}/.github/workflows/on_push.yml +7 -7
  3. {tx_engine-0.11.2 → tx_engine-0.11.3}/Cargo.lock +26 -25
  4. {tx_engine-0.11.2 → tx_engine-0.11.3}/Cargo.toml +5 -2
  5. {tx_engine-0.11.2 → tx_engine-0.11.3}/PKG-INFO +1 -1
  6. {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/Releases.md +1 -0
  7. tx_engine-0.11.3/python/src/tests/test_woc_balance.py +63 -0
  8. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_woc_interface.py +18 -1
  9. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/interface/woc.py +79 -21
  10. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/interface/woc_interface.py +8 -6
  11. tx_engine-0.11.3/src/interface/woc_interface.rs +633 -0
  12. tx_engine-0.11.2/src/interface/woc_interface.rs +0 -361
  13. {tx_engine-0.11.2 → tx_engine-0.11.3}/.github/dependabot.yml +0 -0
  14. {tx_engine-0.11.2 → tx_engine-0.11.3}/.gitignore +0 -0
  15. {tx_engine-0.11.2 → tx_engine-0.11.3}/LICENSE +0 -0
  16. {tx_engine-0.11.2 → tx_engine-0.11.3}/LICENSE-rust-sv +0 -0
  17. {tx_engine-0.11.2 → tx_engine-0.11.3}/README-chain-gang.md +0 -0
  18. {tx_engine-0.11.2 → tx_engine-0.11.3}/README-pypi.md +0 -0
  19. {tx_engine-0.11.2 → tx_engine-0.11.3}/README.md +0 -0
  20. {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/BIP-32-Python.md +0 -0
  21. {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/BIP-32.md +0 -0
  22. {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/Chronicle-Python.md +0 -0
  23. {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/Chronicle.md +0 -0
  24. {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/Development.md +0 -0
  25. {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/Python-API.md +0 -0
  26. {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/README-chain-gang.md +0 -0
  27. {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/README.md +0 -0
  28. {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/Regtest.md +0 -0
  29. {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/Requirements +0 -0
  30. {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/diagrams/keys.png +0 -0
  31. {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/diagrams/keys.puml +0 -0
  32. {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/diagrams/overview.png +0 -0
  33. {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/diagrams/overview.puml +0 -0
  34. {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/diagrams/python_classes.png +0 -0
  35. {tx_engine-0.11.2 → tx_engine-0.11.3}/docs/diagrams/python_classes.puml +0 -0
  36. {tx_engine-0.11.2 → tx_engine-0.11.3}/pyproject.toml +0 -0
  37. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/README.md +0 -0
  38. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/lint.sh +0 -0
  39. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/requirements.txt +0 -0
  40. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/README.md +0 -0
  41. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_bit_twiddling.py +0 -0
  42. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_bsv.py +0 -0
  43. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_chronicle_context.py +0 -0
  44. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_chronicle_opcodes.py +0 -0
  45. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_chronicle_tx_validate.py +0 -0
  46. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_debug.py +0 -0
  47. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_ec.py +0 -0
  48. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_fed.py +0 -0
  49. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_hd_wallet.py +0 -0
  50. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_if.py +0 -0
  51. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_interface.py +0 -0
  52. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_op.py +0 -0
  53. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_parse.py +0 -0
  54. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_rpc_interface.py +0 -0
  55. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_script.py +0 -0
  56. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_sighash.py +0 -0
  57. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_sign.py +0 -0
  58. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_tx.py +0 -0
  59. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_verify_script.py +0 -0
  60. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_wallet.py +0 -0
  61. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_wif_from_pw_nonce.py +0 -0
  62. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tests/test_woc.py +0 -0
  63. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/__init__.py +0 -0
  64. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/engine/__init__.py +0 -0
  65. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/engine/context.py +0 -0
  66. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/engine/cryptography_utils.py +0 -0
  67. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/engine/engine_types.py +0 -0
  68. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/engine/op_code_names.py +0 -0
  69. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/engine/op_codes.py +0 -0
  70. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/engine/util.py +0 -0
  71. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/interface/__init__.py +0 -0
  72. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/interface/blockchain_interface.py +0 -0
  73. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/interface/interface_factory.py +0 -0
  74. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/interface/mock_interface.py +0 -0
  75. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/interface/rpc_interface.py +0 -0
  76. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/interface/verify_script.py +0 -0
  77. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/tx/__init__.py +0 -0
  78. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/src/tx_engine/tx/sighash.py +0 -0
  79. {tx_engine-0.11.2 → tx_engine-0.11.3}/python/tests.sh +0 -0
  80. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/address/mod.rs +0 -0
  81. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/chronicle.rs +0 -0
  82. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/interface/blockchain_interface.rs +0 -0
  83. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/interface/mod.rs +0 -0
  84. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/interface/rpc_interface.rs +0 -0
  85. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/interface/test_interface.rs +0 -0
  86. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/interface/uaas_interface.rs +0 -0
  87. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/lib.rs +0 -0
  88. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/addr.rs +0 -0
  89. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/authch.rs +0 -0
  90. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/block.rs +0 -0
  91. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/block_header.rs +0 -0
  92. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/block_locator.rs +0 -0
  93. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/blocktxn.rs +0 -0
  94. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/cmpctblock.rs +0 -0
  95. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/createstrm.rs +0 -0
  96. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/fee_filter.rs +0 -0
  97. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/filter_add.rs +0 -0
  98. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/filter_load.rs +0 -0
  99. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/getblocktxn.rs +0 -0
  100. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/headers.rs +0 -0
  101. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/inv.rs +0 -0
  102. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/inv_vect.rs +0 -0
  103. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/merkle_block.rs +0 -0
  104. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/message.rs +0 -0
  105. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/message_header.rs +0 -0
  106. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/mod.rs +0 -0
  107. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/node_addr.rs +0 -0
  108. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/node_addr_ex.rs +0 -0
  109. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/out_point.rs +0 -0
  110. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/ping.rs +0 -0
  111. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/protoconf.rs +0 -0
  112. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/reject.rs +0 -0
  113. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/send_cmpct.rs +0 -0
  114. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/streamack.rs +0 -0
  115. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/tx.rs +0 -0
  116. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/tx_chronicle_validate.rs +0 -0
  117. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/tx_in.rs +0 -0
  118. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/tx_out.rs +0 -0
  119. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/messages/version.rs +0 -0
  120. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/network/mod.rs +0 -0
  121. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/network/network.rs +0 -0
  122. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/network/seed_iter.rs +0 -0
  123. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/peer/atomic_reader.rs +0 -0
  124. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/peer/mod.rs +0 -0
  125. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/peer/peer.rs +0 -0
  126. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/python/mod.rs +0 -0
  127. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/python/py_hd_wallet.rs +0 -0
  128. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/python/py_script.rs +0 -0
  129. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/python/py_stack.rs +0 -0
  130. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/python/py_tx.rs +0 -0
  131. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/python/py_wallet.rs +0 -0
  132. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/script/checker.rs +0 -0
  133. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/script/format.rs +0 -0
  134. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/script/interpreter/eval.rs +0 -0
  135. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/script/interpreter/mod.rs +0 -0
  136. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/script/interpreter/multisig.rs +0 -0
  137. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/script/interpreter/push.rs +0 -0
  138. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/script/interpreter/rules.rs +0 -0
  139. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/script/interpreter/script_code.rs +0 -0
  140. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/script/interpreter/tests.rs +0 -0
  141. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/script/mod.rs +0 -0
  142. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/script/op_codes.rs +0 -0
  143. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/script/stack.rs +0 -0
  144. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/test_util.rs +0 -0
  145. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/transaction/mod.rs +0 -0
  146. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/transaction/p2pkh.rs +0 -0
  147. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/transaction/sighash.rs +0 -0
  148. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/bits.rs +0 -0
  149. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/bloom_filter.rs +0 -0
  150. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/bounded_read.rs +0 -0
  151. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/errors.rs +0 -0
  152. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/future.rs +0 -0
  153. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/hash160.rs +0 -0
  154. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/hash256.rs +0 -0
  155. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/latch.rs +0 -0
  156. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/mod.rs +0 -0
  157. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/rx.rs +0 -0
  158. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/serdes.rs +0 -0
  159. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/sha1.rs +0 -0
  160. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/sha256.rs +0 -0
  161. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/util/var_int.rs +0 -0
  162. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/base58_checksum.rs +0 -0
  163. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/extended_key.rs +0 -0
  164. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/hd_wallet.rs +0 -0
  165. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/hd_watch_wallet.rs +0 -0
  166. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/mnemonic.rs +0 -0
  167. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/mod.rs +0 -0
  168. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/wallet.rs +0 -0
  169. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/wordlists/chinese_simplified.txt +0 -0
  170. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/wordlists/chinese_traditional.txt +0 -0
  171. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/wordlists/english.txt +0 -0
  172. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/wordlists/french.txt +0 -0
  173. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/wordlists/italian.txt +0 -0
  174. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/wordlists/japanese.txt +0 -0
  175. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/wordlists/korean.txt +0 -0
  176. {tx_engine-0.11.2 → tx_engine-0.11.3}/src/wallet/wordlists/spanish.txt +0 -0
  177. {tx_engine-0.11.2 → tx_engine-0.11.3}/tests/hostile_lengths.rs +0 -0
  178. {tx_engine-0.11.2 → tx_engine-0.11.3}/tests/regtest_p2p.rs +0 -0
  179. {tx_engine-0.11.2 → tx_engine-0.11.3}/tools/README.md +0 -0
  180. {tx_engine-0.11.2 → tx_engine-0.11.3}/tools/dbg.py +0 -0
  181. {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@v4
51
- - uses: actions/setup-python@v5
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@v4
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@v4
79
- - uses: actions/setup-python@v5
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@v4
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@v4
107
- - uses: actions/setup-python@v5
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@v4
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@v4
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@v4
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@v4
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@v4
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@v4
16
+ uses: actions/checkout@v7
17
17
  - name: Install Python
18
- uses: actions/setup-python@v5
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@v4
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@v4
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@v4
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@v4
96
+ uses: actions/checkout@v7
97
97
  - name: Install Python
98
- uses: actions/setup-python@v5
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.5",
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.6"
139
+ version = "1.4.7"
140
140
  source = "registry+https://github.com/rust-lang/crates.io-index"
141
- checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4"
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.4"
151
+ version = "1.0.5"
152
152
  source = "registry+https://github.com/rust-lang/crates.io-index"
153
- checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
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.2"
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.5",
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.12"
458
+ version = "0.1.13"
458
459
  source = "registry+https://github.com/rust-lang/crates.io-index"
459
- checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
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.9"
679
+ version = "0.27.10"
679
680
  source = "registry+https://github.com/rust-lang/crates.io-index"
680
- checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
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.5",
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.2"
1281
+ version = "0.10.3"
1281
1282
  source = "registry+https://github.com/rust-lang/crates.io-index"
1282
- checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
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.5",
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.5"
1734
+ version = "3.0.6"
1734
1735
  source = "registry+https://github.com/rust-lang/crates.io-index"
1735
- checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
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.5",
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.5",
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.24"
1942
+ version = "1.0.26"
1942
1943
  source = "registry+https://github.com/rust-lang/crates.io-index"
1943
- checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
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.5",
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.5",
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.5",
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.5",
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.2"
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"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tx_engine
3
- Version: 0.11.2
3
+ Version: 0.11.3
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3.11
@@ -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 transations associated with this address"""
63
- return get_response(f"{get_url(testnet)}/address/{address}/unspent")
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 transations associated with this address"""
68
- tx_hash = None
69
- tx_pos = 0
70
- value = 0
71
- url = "{}/address/{}/unspent".format(get_url(testnet), address)
72
- response = requests.get(url)
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
- return get_response(f"{get_url(testnet)}/address/{address}/balance")
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
- WhatsOnChain reports a mempool UTXO as `height: 0`, while a negative height
24
- is what means unconfirmed here and in the Rust crate, so the value is
25
- translated on the way in rather than left for every caller to special-case.
26
- A height of 0 cannot mean the genesis block: the genesis coinbase is
27
- unspendable, so it never appears in an unspent set.
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
- if entry.get("height") == 0:
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