diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 38c9e195..c6d4d5e1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,7 +23,7 @@ jobs: python-version: '3.10' cache: 'pip' - name: Setup Python caches - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ${{ env.pythonLocation }} key: ${{ env.pythonLocation }}-${{ hashFiles('setup.py','requirements.txt','requirements-test.txt') }} diff --git a/.gitignore b/.gitignore index 61731016..df20f122 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ docs/_build .idea/ .env .venv +*.log +*.http diff --git a/electrumx/lib/atomicals_blueprint_builder.py b/electrumx/lib/atomicals_blueprint_builder.py index 53e7b1c7..e1557bc6 100644 --- a/electrumx/lib/atomicals_blueprint_builder.py +++ b/electrumx/lib/atomicals_blueprint_builder.py @@ -616,6 +616,9 @@ def custom_color_ft_atomicals(cls, ft_atomicals, operations_found_at_inputs, tx) # expected_value will equal to txout.value if expected_value > txout.value: expected_value = txout.value + # The coloring value cannot exceed the remaining atomical value. + if expected_value > remaining_value: + expected_value = remaining_value # set cleanly_assigned if expected_value < txout.value: cleanly_assigned = False diff --git a/electrumx/lib/coins.py b/electrumx/lib/coins.py index 29a9bb49..2512f27b 100644 --- a/electrumx/lib/coins.py +++ b/electrumx/lib/coins.py @@ -992,14 +992,21 @@ def warn_old_client_on_tx_broadcast(cls, client_ver): return False +class BitcoinSegwitTestnet(BitcoinTestnet): + NAME = "BitcoinSegwit" # support legacy name + + class BitcoinTestnet4(BitcoinTestnetMixin, AtomicalsCoinMixin, Coin): NAME = "Bitcoin" NET = "testnet4" - DESERIALIZER = lib_tx.DeserializerSegWit - CRASH_CLIENT_VER = (3, 2, 3) - PEERS = [] - GENESIS_HASH = "00000000da84f2bafbbc53dee25a72ae" "507ff4914b867c565be350b0da8bf043" - RPC_PORT = 48332 + PEERS = [ + 'blackie.c3-soft.com s57010 t57009', + 'testnet4-electrumx.wakiyamap.dev', + ] + GENESIS_HASH = ('00000000da84f2bafbbc53dee25a72ae' + '507ff4914b867c565be350b0da8bf043') + TX_COUNT = 1 + TX_COUNT_HEIGHT = 1 ATOMICALS_ACTIVATION_HEIGHT = 27000 ATOMICALS_ACTIVATION_HEIGHT_DMINT = 27000 @@ -1022,7 +1029,7 @@ def warn_old_client_on_tx_broadcast(cls, client_ver): return False -class BitcoinSegwitTestnet(BitcoinTestnet): +class BitcoinSegwitTestnet4(BitcoinTestnet4): NAME = "BitcoinSegwit" # support legacy name diff --git a/electrumx/lib/util_atomicals.py b/electrumx/lib/util_atomicals.py index 7d98bed0..6959283f 100644 --- a/electrumx/lib/util_atomicals.py +++ b/electrumx/lib/util_atomicals.py @@ -1450,6 +1450,10 @@ def auto_encode_bytes_elements(state): for key, value in state.items(): state[key] = auto_encode_bytes_elements(value) + # Handles unknown undefined type. + if type(state).__name__ == 'undefined_type': + return None + return state @@ -1468,6 +1472,10 @@ def auto_encode_bytes_items(state): reformatted_list.append(auto_encode_bytes_elements(item)) return reformatted_list + # Handles unknown undefined type. + if type(state).__name__ == 'undefined_type': + return None + cloned_state = {} try: if isinstance(state, dict): diff --git a/electrumx/server/block_processor.py b/electrumx/server/block_processor.py index b0e93432..ca32b0d9 100644 --- a/electrumx/server/block_processor.py +++ b/electrumx/server/block_processor.py @@ -580,8 +580,6 @@ def get_general_data_with_cache(self, key): cache = self.general_data_cache.get(key) if not cache: cache = self.db.get_general_data(key) - if cache: - self.general_data_cache[key] = cache return cache # Get the mint information and LRU cache it for fast retrieval @@ -3529,7 +3527,7 @@ def build_atomicals_spent_at_inputs_for_validation_only(self, tx): # Builds a map of the atomicals spent at a tx # It uses the spend_atomicals_utxo method but with live_run == False - def build_atomicals_receive_at_ouutput_for_validation_only(self, tx, txid): + def build_atomicals_receive_at_output_for_validation_only(self, tx, txid): spend_atomicals_utxo = self.spend_atomicals_utxo atomicals_receive_at_outputs = {} txout_index = 0 diff --git a/electrumx/server/session/http_session.py b/electrumx/server/session/http_session.py index c523d1c6..c05a488c 100644 --- a/electrumx/server/session/http_session.py +++ b/electrumx/server/session/http_session.py @@ -241,10 +241,10 @@ async def donation_address(self): """Return the donation address as a string, empty if there is none.""" return self.env.donation_address - async def server_features_async(self): + def server_features_async(self): return self.server_features(self.env) - async def peers_subscribe(self): + def peers_subscribe(self): """Return the server peers as a list of (ip, host, details) tuples.""" return self.peer_mgr.on_peers_subscribe(False) diff --git a/electrumx/server/session/session_base.py b/electrumx/server/session/session_base.py index 5991c805..a92f7945 100644 --- a/electrumx/server/session/session_base.py +++ b/electrumx/server/session/session_base.py @@ -7,6 +7,7 @@ NewlineFramer, ReplyAndDisconnect, Request, + RPCError, RPCSession, handler_invocation, ) @@ -119,14 +120,17 @@ async def handle_request(self, request): """Handle an incoming request. ElectrumX doesn't receive notifications from client sessions. """ + method = request.method if isinstance(request, Request): - handler = self.request_handlers.get(request.method) - method = request.method - args = request.args + handler = self.request_handlers.get(method) else: handler = None - method = "invalid method" - args = None + if handler is None: + from aiorpcx import JSONRPC + self.logger.error(f'Unknown handler for the method "{method}"') + return RPCError(JSONRPC.METHOD_NOT_FOUND, f'Unknown handler for the method "{method}"') + + args = request.args self.logger.debug(f"Session request handling: [method] {method}, [args] {args}") # If DROP_CLIENT_UNKNOWN is enabled, check if the client identified @@ -136,8 +140,15 @@ async def handle_request(self, request): raise ReplyAndDisconnect(BAD_REQUEST, "use server.version to identify client") self.session_mgr.method_counts[method] += 1 - coro = handler_invocation(handler, request)() - if isinstance(coro, Awaitable): - return await coro - else: - return coro + + # Wraps all internal errors without closing the session. + try: + result = handler_invocation(handler, request)() + if isinstance(result, Awaitable): + result = await result + return result + except BaseException as e: + import traceback + stack = traceback.format_exc() + self.logger.error(f"Session request error: [method:{method}], [args:{args}], [error:{e}], [stack:{stack}]") + return RPCError(-1, str(e)) diff --git a/electrumx/server/session/session_manager.py b/electrumx/server/session/session_manager.py index 4952fc26..aa286155 100644 --- a/electrumx/server/session/session_manager.py +++ b/electrumx/server/session/session_manager.py @@ -7,7 +7,7 @@ from collections import defaultdict from functools import partial from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network -from typing import TYPE_CHECKING, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional import attr import pylru @@ -47,7 +47,7 @@ from electrumx.server.session import BAD_REQUEST, DAEMON_ERROR from electrumx.server.session.http_session import HttpSession from electrumx.server.session.rpc_session import LocalRPC -from electrumx.server.session.util import SESSION_PROTOCOL_MAX, non_negative_integer +from electrumx.server.session.util import SESSION_PROTOCOL_MAX, assert_tx_hash, non_negative_integer from electrumx.version import electrumx_version if TYPE_CHECKING: @@ -953,7 +953,7 @@ async def transaction_decode_raw_tx_blueprint( }, } elif op == "nft": - _receive_at_outputs = self.bp.build_atomicals_receive_at_ouutput_for_validation_only(tx, tx_hash) + _receive_at_outputs = self.bp.build_atomicals_receive_at_output_for_validation_only(tx, tx_hash) tx_out = tx.outputs[0] atomical_id = location_id_bytes_to_compact(_receive_at_outputs[0][-1]["atomical_id"]) mint_info = { @@ -991,7 +991,7 @@ async def transaction_decode_raw_tx_blueprint( # Analysis the transaction detail by txid. # See BlockProcessor.op_list for the complete op list. async def get_transaction_detail(self, tx_id: str, height=None, tx_num=-1): - tx_hash = hex_str_to_hash(tx_id) + tx_hash = assert_tx_hash(tx_id) res = self._tx_detail_cache.get(tx_hash) if res: # txid maybe the same, this key should add height add key prefix @@ -1011,7 +1011,7 @@ async def get_transaction_detail(self, tx_id: str, height=None, tx_num=-1): operation_found_at_inputs = parse_protocols_operations_from_witness_array(tx, tx_hash, True) atomicals_spent_at_inputs = self.bp.build_atomicals_spent_at_inputs_for_validation_only(tx) - atomicals_receive_at_outputs = self.bp.build_atomicals_receive_at_ouutput_for_validation_only(tx, tx_hash) + atomicals_receive_at_outputs = self.bp.build_atomicals_receive_at_output_for_validation_only(tx, tx_hash) blueprint_builder = AtomicalsTransferBlueprintBuilder( self.logger, atomicals_spent_at_inputs, @@ -1043,62 +1043,6 @@ async def get_transaction_detail(self, tx_id: str, height=None, tx_num=-1): "is_cleanly_assigned": is_cleanly_assigned, }, } - operation_type = operation_found_at_inputs.get("op", "") if operation_found_at_inputs else "" - if operation_found_at_inputs: - payload = operation_found_at_inputs.get("payload") - payload_not_none = payload or {} - res["info"]["payload"] = payload_not_none - if blueprint_builder.is_mint and operation_type in ["dmt", "ft"]: - expected_output_index = 0 - tx_out = tx.outputs[expected_output_index] - location = tx_hash + util.pack_le_uint32(expected_output_index) - # if save into the db, it means mint success - has_atomicals = self.db.get_atomicals_by_location_long_form(location) - if len(has_atomicals): - ticker_name = payload_not_none.get("args", {}).get("mint_ticker", "") - status, candidate_atomical_id, _ = self.bp.get_effective_ticker(ticker_name, self.bp.height) - if status: - atomical_id = location_id_bytes_to_compact(candidate_atomical_id) - res["info"] = { - "atomical_id": atomical_id, - "location_id": location_id_bytes_to_compact(location), - "payload": payload, - "outputs": { - expected_output_index: [ - { - "address": get_address_from_output_script(tx_out.pk_script), - "atomical_id": atomical_id, - "type": "FT", - "index": expected_output_index, - "value": tx_out.value, - } - ] - }, - } - elif operation_type == "nft": - if atomicals_receive_at_outputs: - expected_output_index = 0 - location = tx_hash + util.pack_le_uint32(expected_output_index) - tx_out = tx.outputs[expected_output_index] - atomical_id = location_id_bytes_to_compact( - atomicals_receive_at_outputs[expected_output_index][-1]["atomical_id"] - ) - res["info"] = { - "atomical_id": atomical_id, - "location_id": location_id_bytes_to_compact(location), - "payload": payload, - "outputs": { - expected_output_index: [ - { - "address": get_address_from_output_script(tx_out.pk_script), - "atomical_id": atomical_id, - "type": "NFT", - "index": expected_output_index, - "value": tx_out.value, - } - ] - }, - } async def make_transfer_inputs(result, inputs_atomicals, tx_inputs, make_type) -> Dict[int, List[Dict]]: for atomical_id, input_data in inputs_atomicals.items(): @@ -1146,6 +1090,8 @@ def make_transfer_outputs( result[k].append(_data) return result + operation_type = operation_found_at_inputs.get("op", "") if operation_found_at_inputs else "" + # no operation_found_at_inputs, it will be transfer. if blueprint_builder.ft_atomicals and atomicals_spent_at_inputs: if not operation_type and not op_raw: @@ -1158,6 +1104,66 @@ def make_transfer_outputs( await make_transfer_inputs(res["transfers"]["inputs"], blueprint_builder.nft_atomicals, tx.inputs, "NFT") make_transfer_outputs(res["transfers"]["outputs"], blueprint_builder.nft_output_blueprint.outputs) + if operation_found_at_inputs: + payload = operation_found_at_inputs.get("payload") + payload_not_none = payload or {} + res["info"]["payload"] = payload_not_none + # Mint operation types are "dmt", "nft", "ft", "dft". "dft" is the deploy operation. + if operation_type in ["dmt", "ft"]: + expected_output_index = 0 + tx_out = tx.outputs[expected_output_index] + location = tx_hash + util.pack_le_uint32(expected_output_index) + # if save into the db, it means mint success + has_atomicals = self.db.get_atomicals_by_location_long_form(location) + if len(has_atomicals): + ticker_name = payload_not_none.get("args", {}).get("mint_ticker", "") + status, candidate_atomical_id, _ = self.bp.get_effective_ticker(ticker_name, self.bp.height) + if status: + atomical_id = location_id_bytes_to_compact(candidate_atomical_id) + res["info"] = { + "payload": payload, + "outputs": { + expected_output_index: [ + { + "address": get_address_from_output_script(tx_out.pk_script), + "atomical_id": atomical_id, + "location_id": location_id_bytes_to_compact(location), + "type": "FT", + "index": expected_output_index, + "value": tx_out.value, + } + ] + }, + } + elif operation_type == "nft": + if atomicals_receive_at_outputs: + outputs: Dict[int, List[Dict[str, Any]]] = {} + for expected_output_index, atomicals_receives in atomicals_receive_at_outputs.items(): + receives: List[Dict[str, Any]] = [] + for atomicals in atomicals_receives: + atomical_id = location_id_bytes_to_compact(atomicals["atomical_id"]) + if any( + any(output.get("atomical_id") == atomical_id for output in outputs_list) + for outputs_list in res["transfers"]["outputs"].values() + ): + continue + location = tx_hash + util.pack_le_uint32(expected_output_index) + tx_out = tx.outputs[expected_output_index] + receives.append({ + "address": get_address_from_output_script(tx_out.pk_script), + "atomical_id": atomical_id, + "location_id": location_id_bytes_to_compact(location), + "type": "NFT", + "index": expected_output_index, + "value": tx_out.value, + }) + if len(receives) > 0: + outputs[expected_output_index] = receives + res["info"] = { + "payload": payload, + "outputs": outputs, + } + ( payment_id, payment_marker_idx, @@ -1177,7 +1183,7 @@ def make_transfer_outputs( return auto_encode_bytes_elements(res) async def get_transaction_detail_batch(self, tx_ids: str): - tasks = [self.get_transaction_detail(txid) for txid in tx_ids.split(',')] + tasks = [self.get_transaction_detail(assert_tx_hash(tx_id)) for tx_id in tx_ids.split(',')] details = await asyncio.gather(*tasks) return details diff --git a/electrumx/server/session/shared_session.py b/electrumx/server/session/shared_session.py index bb74b03f..996aeaa4 100644 --- a/electrumx/server/session/shared_session.py +++ b/electrumx/server/session/shared_session.py @@ -771,21 +771,21 @@ async def atomicals_get_container_items(self, container, limit, offset): } } - async def atomicals_search_tickers(self, prefix=None, reverse=False, limit=100, offset=0, is_verified_only=False): + def atomicals_search_tickers(self, prefix=None, reverse=False, limit=100, offset=0, is_verified_only=False): if isinstance(prefix, str): prefix = prefix.encode() return self._atomicals_search_name_template( b"tick", "ticker", None, prefix, reverse, limit, offset, is_verified_only ) - async def atomicals_search_realms(self, prefix=None, reverse=False, limit=100, offset=0, is_verified_only=False): + def atomicals_search_realms(self, prefix=None, reverse=False, limit=100, offset=0, is_verified_only=False): if isinstance(prefix, str): prefix = prefix.encode() return self._atomicals_search_name_template( b"rlm", "realm", None, prefix, reverse, limit, offset, is_verified_only ) - async def atomicals_search_subrealms( + def atomicals_search_subrealms( self, parent, prefix=None, @@ -808,7 +808,7 @@ async def atomicals_search_subrealms( is_verified_only, ) - async def atomicals_search_containers( + def atomicals_search_containers( self, prefix=None, reverse=False, limit=100, offset=0, is_verified_only=False ): if isinstance(prefix, str): @@ -1007,7 +1007,7 @@ async def transaction_get(self, tx_hash, verbose=False): tx_hash: the transaction hash as a hexadecimal string verbose: passed on to the daemon """ - assert_tx_hash(tx_hash) + tx_hash = assert_tx_hash(tx_hash) if verbose not in (True, False): raise RPCError(BAD_REQUEST, '"verbose" must be a boolean') diff --git a/electrumx/version.py b/electrumx/version.py index da4c9e56..9b42a102 100644 --- a/electrumx/version.py +++ b/electrumx/version.py @@ -1,4 +1,4 @@ -__version__ = "1.5.1.1" +__version__ = "1.5.2.0" electrumx_version = f"ElectrumX {__version__}" electrumx_version_short = __version__ diff --git a/tests/blocks/bitcoin_testnet4_40000.json b/tests/blocks/bitcoin_testnet4_40000.json new file mode 100644 index 00000000..11f7baa9 --- /dev/null +++ b/tests/blocks/bitcoin_testnet4_40000.json @@ -0,0 +1,29 @@ +{ + "hash": "000000000000000c1a1fad82b0e133f4772802b6dff7a95990580ae2e15c634f", + "size": 5826, + "height": 40000, + "merkleroot": "cad0d11c8d5e1304a36a882827e32801142d6a290ce1a2a4e8f0035bfa7f6d45", + "tx": [ + "5c50d460b3b98ea0c70baa0f50d1f0cc6ffa553788b4a7e23918bcdd558828fa", + "fb4e96dad663f44fafbb1277cdfe2d7dbe50e03c99d527f5bff3ac9b4c3570fc", + "d991f3762c955ed327608c7edd1588c741e3369b0fe63d58eae9fd3959eef342", + "e451e0a76f1326d657895a5cfc4fd0be67cc96fdc7bb416b81d55f0b5d210c53", + "8bce727fe08fbb79f02682f71d0b1f33a79039cd7a70623a51945bf2dc86d77c", + "d1f636b56a494ffcb789682ec1db60d5be4100d3e6d760ab6f1a3ca2d7ff2485", + "285f28586b364ccbb35c0e092b6b16615f2cc31f3c1176ec52ed65b19153bf8a", + "659ff9dab924a0e1236e1b465cbd84195564ae297968028037fcab656faefba3", + "b86c427253faf575e49e54643962b0a8e2065289848e8616247a6b198225b5aa", + "4cafc859ac10dd7d297d7498d7635158ad45045312227b593b43b274e33674cc", + "3bce55005ce35f3cf632bbc29a215f7ccf2480194b7fa29c93b2d9693964d3d0", + "ca413bf56518af95872b305fb56502d465bbd605adaeffe086f38c934993dfd9", + "28771e00a44951c2f7d905653bc6afbc6dbefeccbad65c199388e05f54e127dc", + "eb948c9de926abccb9f65bb1e8c98906bd8b0e1103dc39d6a6f4e9b7b74143f0", + "ee75ac577088e34bd552b4aa3e7b4bf1b6528b6b395bcf713f44916aa9a5a2f1", + "3f792690080f2b03d7d5a0ea7e1d7d79d83242024043b685a3ab474f71dd4efd" + ], + "time": 1723908842, + "nonce": 1825621086, + "bits": "1954fa04", + "previousblockhash": "00000000000000124f9e271251c750dbe30d9b3afe9f06fbcfb2fe3baa160203", + "block": "0020b42b030216aa3bfeb2cffb069ffe3a9b0de3db50c75112279e4f1200000000000000456d7ffa5b03f0e8a4a2e10c296a2d140128e32728886aa304135e8d1cd1d0caeac2c06604fa54195ec4d06c10010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff4a03409c000004ccc2c06604389abb271024e1b5660000000000000000000000000a636b706f6f6c22506f72746c616e642e484f444c207c20446574726f6974207c20f09f87baf09f87b8ffffffff02fe01062a010000001976a9142ce72b25fe97b52638c199acfaa5e3891ddfed5b88ac0000000000000000266a24aa21a9ed3e249fa3559a30d20a840b05239f365bb7d93aa6cb5484f169ac8badea0e843501200000000000000000000000000000000000000000000000000000000000000000000000000100000000010143da142315ac08a9f9b04d456d51175c18951a6939c972382ac5e1e6a139038b0000000000ffffffff019c6108000000000016001463841fbc7aeaab5104602a378dd66e72261fef300247304402204974a564116d4537e140859d922ab821aaca036221e2d507ef92b96adaa6cdb6022021ab91e5c86e76c2f7ea4a6b36a3d5a5ab4643da808f1219d7d06903b5827a1601210204cab25185dc08801da7cf5d62e7daa92914d69665a22e03e9ecc30ea02fa7660000000002000000000104e404664445a549e21460911648e6c68b958da123ce455db1b6244bd28886d2320000000017160014ed62876440f9432d9f6bfc48d59cef0a24214c31fdffffff8e32e9d7a682a856bd8e6feaf70458c3031d6a6b64ca6047bd9ad3c066d350990100000017160014c9a8d47f08dfc91a863111af39291486f09b534ffdffffff04b2752cab3d1409ced16e9656985b2b724cb1a6d8c264fd3e0c44072b0902c701000000171600144e140ec7d2f05008e5f785fb9a058f85429f20cffdffffff8a6929fd7d22f5634971620b6d5e2932a7ab60cc70e5f0509d7260b5054c28b90000000017160014a735c6fb24355366304ca88b867bc3a6c8c4852bfdffffff037b710e00000000001600147dd92898c488a0838e5ea32b9fb57d2a49f14b3da0e20e00000000001976a914a0a3a6eab4357f6cade3b5e0031f13564ef9162f88ac80ca07000000000016001463bbdedda0703c995a632a1a4ee92ffe09af4f6e0247304402205028aba5260d08db3444ddeb8f7a56e1b7278c8071ed7e8d37078a9d9ea6033102204c9c15ba2bb2516c8365bda4118ac189f1fda54ed38a4d7002e5d7fa7d74c18b012102ec7430e29962705708224aa88169072fec1094badb1103213939b84ba2ea734a0247304402204f7f475ec40edba8fa12cd30e04752e6c45937a976f93f9076dcf04361513ae702207363038de2d9fc2de7744ee2e1cf7548ca6ce9979c81f6c0ece495ffa2cc646f0121027aa0626d3fff010cbc240a25c5806c717b70523e0601737d4645ce7a4acee88a0247304402201b1784f21a2376b3578f3a193449a86050edd59f1be9aa0da30e9c808844e24602205102ed3ce00cb9fb6d56e642c0ed0c7a460ed6be4592de8bec23793a0d8c72e30121036e4bce262c7b4520b7012d84424324c9b093b17559133cbea66acb798274db850247304402201dca7669b20a6cb677c2f44e9349bd96472a26aa09ada0d3cba68f77ad1bc514022075f309d836af6195c677999659e86d5e80e196c2a7ccda68bc30b1ce940113a60121034d2b3d15f3b7856a7c350748ae48223dd05287c682bce97e6b198ff4ab9b61bf3f9c0000020000000001017a18376793c140241311576124240c41027ffa1d1b1d3e64bbdadf6b4fbe4f910000000000fdffffff02fe42d028170000001600147b458433d0c04323426ef88365bd4cfef141ac7520a10700000000001600147d237cb1fe62153805fa1fca6cbc74765ccaeb5402473044022022784ac1c15c76b320274fcf99cafa55ba1554d0e742f79d5bb7ecd2f3725e9f02205f193c7f36742aa1f23b091d2191208256e33d6084eed65230f5f238432e03370121030db9616d96a7b7a8656191b340f77e905ee2885a09a7a1e80b9c8b64ec746fb30000000002000000000101b2a2b9cbcd6acd886a3adcc7e172d7cdece234a8f4502079ed2d01fa46322f000100000000fdffffff02246d5d13000000001600140633f9fe93dd480f20159602e4e07e1721b7e4c03e6900000000000016001464de410cc361cee4f8b506567df3ca9f30710067024730440220680ae471f2afe3dfd90b276e686e191525f8a36e1e87187d1742c8b5c1b17b020220173a293064b41c70cb58bb1a3715dafbc56fc0a786c15415c9cdda9b237a86fc01210396867c45cc97e85d704a9afdebb388bc6feffa4851dda31168ec2fd7cf5763663f9c00000200000003234711ca14f70dbd873b8606ad0978c82928aa9eaafa6ac5291b6b25789862f9010000006a4730440220028ac25e04a859ba1f7a3ea8f4bdc175adaadd8ce8575d53fa9db37d5ac153e9022046344c62f2e5ddce6ec2f30cbb2ddeb6495621297c645b4948093d6712b586d50121039f4c6ee26308a34cd14a1930fa60a8415378355596c1153295e5275dff66fd90fdffffff0ff3701d18c63d4daea337060f47ade86466e8af46aa918827cc63e3482ae937010000006a473044022025897f1f067f6cbd818fb114a89dd9ce27de00842092f9eaa45374b820f320ee022045670f35a7b15c7e4b519445460ee3cac1ecc93a3bb858a892c9aab46c5ddf7f01210300660d1d4fc56e0b8e203e6cb667f7f78c04fa012cc544686b16a16b9a1c31c8fdffffffa70f23cb877d1eb479a420855fea388e9039a26b5f408d30de59d28bd15e4dcb020000006a47304402206055c9f81ab41b1ed3933422add9949535103e5aba7a8eaaa3618ae85d1a4999022008d3fc627703e6778f6d401b10f1fd71a6f511255cbf5f3a89f9c508ef50f37f0121027aca30f737a95a53f3da28cde58c3517860f8a51f850eb699f7d2bb9039cd46efdffffff0316fc040000000000160014f45ea236de6fd4e2ed8a6e6320cdf2db173c6ee0c1040200000000001976a9147dbe4c26a474503bd6f576c7b36c31d37861df6088ac892d090000000000160014f66ac1d07ccc33d517e26f9dcc00eb6468709105e89b0000020000000001010bcdd5e36694495e4a4d2f746d66388f276919dc84a7bbc612b543f48b36f40c0000000000fdffffff026bbd7f07000000001600147b458433d0c04323426ef88365bd4cfef141ac7520a1070000000000160014989792062dc8cab87dc45f47718950b773ca7d3902473044022050398f98e96da6b4b8cadee89c1eb5145a7db044711b4880e1bf66b2b15e850f0220764043c517a1df9dcdc45fd62c381e94e654eece565cacc53bc5acdd882c511e0121030db9616d96a7b7a8656191b340f77e905ee2885a09a7a1e80b9c8b64ec746fb30000000002000000000103421439343bda7fe764f901845cf7f0c751f186b25e98495c186da39fe751c5f601000000171600141bb112e56f0574f06b72688a12f670a9ffbf42d0fdffffff1dc67c688456f72349792804d2bab77aa60563dfba7405076ec1e63e58f41c620000000017160014a4244c5b94e5e11fd5227377495173f38bc51261fdffffff04b2752cab3d1409ced16e9656985b2b724cb1a6d8c264fd3e0c44072b0902c70000000017160014985c9b1ad953d7bee7533b599dec0d7eb291b9a2fdffffff03b53f00000000000017a9140f1e74ec42ed483d18c463d51bfc5d25b0ffe6b88719f308000000000017a914e7f55f5bd23c4396dcbea8cca6a1819e6d42653087ac2208000000000017a914e1cdf73c9251b789c89022f0760b3a285361d885870247304402200bcf968fca75caaed61f2fd5a3ec607f1c994c0d22cc242921ef532afb0ee2f2022065c95b63b1b357aee5027073bd11d95de5c19f200a87e17165a772e683b4e6c2012103296acc3caefa5e3af62ff8abfd51bf62766941c4b3456098ab20e28f1df1bd5602473044022070836855ba7cf3d66d162c5b78b966003fc17158d5c2ce75faa6f0c709d21da10220492887f041840dc064ccd1a9d9a2f7cfe1e8b7e98bbeaa3aeafd7c3f40642620012102838d41dda4292ee4458969a89dddc6f38f1251ded8c2177a89f61134e0a8552c02473044022065bee42a7eaff5e100ec3d273169b8940cd1f33091caec7dd0cb6c0e2a7560ac02206bc54dae0a001f73001ec5b3c1d6f2483fb6f0214b022399a6994def6635e2030121031aca61b28388afc8446ed57fc5db1cd114f903920d89e94bb818ae12bf4ba1703f9c000002000000000103db5d53cbe8502f4403bd705db7867702dc99d4736deb9b6b5b9507f607cea80c01000000171600146b9bfb89dbdf9c077d7d494a679c90d4a85d4f3cfdffffffe404664445a549e21460911648e6c68b958da123ce455db1b6244bd28886d2320100000017160014c042680868a35165b194816da26217614c93a201fdffffff652213c7259207a84bf193ad0b28f7847054f351ad5ccd49ed025d70105795380200000017160014a546a713910ba0640c7d5c50462288cf41f075a8fdffffff03865e0200000000001976a9147f558c20d7e485a084a80f792dc14453f3d4189088aca0e20e00000000001976a914da9d72caea1f4e1e2c61483dd4e95d1171c922a788ac19f30800000000001976a9144686ab37c08f93f96dc3b822c08da0b2ac4c4c7088ac024730440220151b48153be2c36d6e4492511e8e7d89fce99352b0fd096f4242066b3ff4de3502205c206c90baf6c0a635f0eabc9088e6320e66993780fdeabfecdd14111c6c9c2b012102fbd6c3e6570d6e5f5733108fc21af8a98e5706a282e17fdd49a112d9f587939702473044022037bd3f3308b027630b9b0da62e17966293ab20929aa9ad3f47bdf5f7ec40bbec0220116cf4f8728bb27218f01bafeebde2b8fe7c78a90aeca2af1ad940cc6edf2b1201210223e709ebd92a74215c0307a96e7ad75e86aa98b79d145e92a7759230d067e0d30247304402207fe9f89dd7bafff12b7fe1ca6b6138ad08b8bda2550b9ed9633986ecac19dff3022061c2181f34e7c2b00f95b6ef29d6a977c0123eea800af114488ce8f88b909c7e012103190b45b10e97e021cf37a9c83d6a33b8d6f49a681c801ceeedd3b3aaa59009af3f9c000002000000000101de1b7652d254bf5165dcc51cbf0239bfea2cc9d4a6d76d1c20855dd08d1ac55e0000000000fdffffff02f3cc3f21010000001600147b458433d0c04323426ef88365bd4cfef141ac7520a10700000000001600144db7aca965c9567d5c1f87aca694dd598a09036b0247304402200c60de020be5c1a68e78bb087c3f00fe0da1ad7ea44294726e87e55c5d8cc9cd022023964060b91443948e26d38d310aeda24c7a507a40f4d0bd765094b36a8487c60121030db9616d96a7b7a8656191b340f77e905ee2885a09a7a1e80b9c8b64ec746fb300000000020000000001014cfa93c868e8e02dd18a872e8fe3bf51d28fe6e107ce9bbf80b70b65ac0ff2c60100000000fdffffff0260680000000000001600141dab0d8b807e7130fd61537bbea8ef6272d50341262de60b00000000160014eea78ae6143721c65f3929e71126b93aaed251800247304402201312a9a35a408bbb89313969e03aa47177eb5a384fb10876f6a6ecd93ea2654b02206d1db64958af07ca42e4f197b0789d27fa5070c5d91c85205f3c6516332107680121025de6139cd7311ffe39c90bfa4981d0bd656699d1483337d2bdb33a9f55c9a0253f9c00000200000003181096155d198c9f73050d3f48cffe3bd0c689fbaf5e2c4d17754f5dd3c76bd7000000006a473044022023dab87e2924e966aafef268773c6d805c24aaf3c09a13f4b331f37eaa77544002200f6ed53ff1e4736db9247110323736861b254800683793ad32a473834c4d43bb0121029627f067fa1c2951781cba0b3c4697319b99e2728e7fa448f397fe71eefef6a1fdffffffad94349376a8b89d447b6277243c5fce43a7327acf1b25b90729aac77e139695010000006a4730440220791495bb35e2c07c7e16f35b6ac2e04eb653517fe4d447166c9c26567bfa0f5e02207abd7ccd39b109531e318bcf983b57920bf8f958dc14b57eb643bedbab55d451012103281fb57ad0b79136efcee94c116d988ccafda362bf1c58be2ca0351efd611b71fdffffff1e066a33bf7de35740a9200e76dfb6ea4ece20353f946d55a0057b413d08e45c000000006a47304402204d6c6a8bd99099c1e0bf816cfafc3e565330115fdeed89996145d5544bf7780802206fa823e474e419ba1ced08c470d83abbecafedc74ad9ed567de5e9e4ecd7e2ee0121022028f113d888fe9bd10531f67f245edb9eedb9a0f1f81dd2e8eb1e97c2be2ed9fdffffff0303ed0800000000001976a9140be140309ef888e4c1404fba019068f4f8fee4b888ac892d09000000000017a914da49c0afb58fd3b75daafbe88f2c75e442a1ab2887e74a01000000000017a914553e979b1011d0200c1ac76aa20c53aa2f52b07a873f9c000002000000000101fe630901d98884a24c5355a1cc4504173a8168aee31af49d69d500b7c4a701760000000000fdffffff028b9000000000000016001460662a32e479a04067d5377b2ffc9d09880bb5a041af2213000000001600140a5ac2337ed3971c6e38c1f37537541c4a2d6b860247304402202add082290b0dbf3d579179cf8f3db58e7768503fe9337f6300a2be98b6a4f0d02200e3773b056278092a352e2780458503e022d25972d6a601fe169543e3cd9fa6f012102aa3410167cecc224a443bce8e854646202387b07d495aa42e9611f0bbc0960433e9c00000200000002cf42974c213ef42de09ae2cf85c2e8b96d83ba63cd0aeb549d01ddbe1a4af069020000006a47304402207502dc3e75dfdc44d25fcc3f8208221b5997f7fb309226267290b07b96f3808202207455f0192ff484f37b197483abc0e923642834c2e8e6664f64a1553494fc3441012102eb4eccaf062182f7f10585cbb47d391a341af8f9e9c43c08a607f3f7e60e28a1fdffffff181096155d198c9f73050d3f48cffe3bd0c689fbaf5e2c4d17754f5dd3c76bd7010000006a473044022027f0996a10816e83a54071654b5cdba017a3fc2d1f339fc22f6c9a336bc4850002202d13710bd0791aad8ffe1fbcb40ab070214dd730599b7f538b149554816d9a04012102fe7120ab2cf852dafdd40634d2787a15d9aa418deed0f5fcc0270b88de250913fdffffff033ff2090000000000160014f671dc027d7700b27a4ffc46d271341b6d116bb9a4450500000000001976a9140b0c195da6df222fd3736597b16f6ae995b4b00988ace6860200000000001600148e115a0b49eee020356cea8f0c5173d2b8306f313f9c000002000000000101c1662773863c60aeee798c12208c3900312dc17e2fb99ff0a5d2e7d76e33d79b0000000000fdffffff02c24d1c090000000016001477483494c166dc8f763eeda8a7910471a2942d996880000000000000160014a4751ecb1f1a9372a2cc3127babab06697e2fcd002473044022024f365d6f4fb6d50cf922a3532a5944c65926703337ef262a373d904585f22bd0220078870f318c7153f8fe526d4fd63b4e8d0551923b6d406da1b6948f2dff5573701210263a93472fb83da0ff8496390ed80810cfee3a94404dcc36322f2213ad097c5a53f9c000002000000000101ff653d23f6fa98659dd87c3c12d878aa6525ea876c09992f64487331414ba86d0100000000fdffffff02ec73000000000000160014abb6664c56fbc8a51e80313e4495764e7de647dfb9b4b81100000000160014c5638b4713c9517e3ba511f13324edd7cceeedc302473044022068313c6de4a547ad4b26ccb64bb66899302b5fc596eee1cc6bd328cf45c7536502205569bc98cde3067f5037c8eda811d930356c48361166677b3e5212e6fd1f8903012103425a5d8f185c9117d121eb909e9fbd739fe46d9a593ec2df96a9e11f4e0e27da3f9c0000" +} diff --git a/tests/lib/test_atomicals_blueprint_builder.py b/tests/lib/test_atomicals_blueprint_builder.py index a17242a0..de240d71 100644 --- a/tests/lib/test_atomicals_blueprint_builder.py +++ b/tests/lib/test_atomicals_blueprint_builder.py @@ -3,6 +3,7 @@ from electrumx.lib.atomicals_blueprint_builder import AtomicalsTransferBlueprintBuilder from electrumx.lib.coins import Bitcoin from electrumx.lib.psbt import parse_psbt_hex_and_operations +from electrumx.lib.tx import TxOutput from electrumx.lib.util_atomicals import ( location_id_bytes_to_compact, parse_atomicals_operations_from_tap_leafs, @@ -1696,6 +1697,78 @@ def mock_mint_fetcher(self, atomical_id): assert blueprint_builder.get_are_fts_burned() is False +def test_custom_colored_ft_overflow(): + raw_tx_str = "0100000000010213ac24b68388e0e32f3b19e95764c67d03b151d1f524eb07bc6e4f2790a3b7f00000000000ffffffff2423c79220c41bd904699aada54868e5c5aecb15168971964c6f5950a7b1d6860000000000ffffffff03e80300000000000022512011b6ce99eab0d8873d787e99e68a351358228893cdf1049ac48aae51391598abe80300000000000022512011b6ce99eab0d8873d787e99e68a351358228893cdf1049ac48aae51391598abe80300000000000022512011b6ce99eab0d8873d787e99e68a351358228893cdf1049ac48aae51391598ab03401aaa5ca0d475dcec02867f28f687494a639b3b43aff0a776c68d94f8cd3e987bb08a3463d8ab937f18f5dadfc916337b2df98cdd700b8514c6fdaff7f5ddffc975201764381bc0b54064cc55a0dda055c5e9875e5cdd7a7c1452d9b93d6015546170ac00630461746f6d017948a178423935323765666134333236323633366438663539313766633736336662646430393333336534623338376166643664346564376139303561313237623237623469301903e86821c01764381bc0b54064cc55a0dda055c5e9875e5cdd7a7c1452d9b93d60155461700140101db7c999f69c7f551d6800341a75ae659e8c100d1bb116b0935afc9ac3aec69bb97eed3ea72fa75912401400aa53f85f8a862f0f672620f31c5e704d8b4d5c00000000" + raw_tx = bytes.fromhex(raw_tx_str) + tx, tx_hash = coin.DESERIALIZER(raw_tx, 0).read_tx_and_hash() + # Manually raise the output value to simulate filling sats. + tx.outputs[0] = TxOutput(value=1200, pk_script=tx.outputs[0].pk_script) + tx.outputs[1] = TxOutput(value=600, pk_script=tx.outputs[1].pk_script) + tx.outputs[2] = TxOutput(value=600, pk_script=tx.outputs[1].pk_script) + + subject_atomical_id1 = ( + b'\x13Jv:\xb1\xad\x9a\xaf\x8a#[7\xa9s\xc0\xcc\xb2\xca\xe1"\x05Y\xc8s\x87\x11\xcc\x90W\xe2\x88\x88\x00\x00\x00' + b"\x00" + ) + subject_atomical_id1_compact = location_id_bytes_to_compact(subject_atomical_id1) + subject_atomical_id2 = ( + b"\xb4'{\x12Z\x90z\xed\xd4\xd6\xaf\x87\xb3\xe43\x93\xd0\xbd?v\xfc\x17Y\x8fmcb2\xa4\xef'\x95\x00\x00\x00\x00" + ) + subject_atomical_id2_compact = location_id_bytes_to_compact(subject_atomical_id2) + # Input only 1000 atomical value for each FT. + atomicals_spent_at_inputs = { + 1: [ + { + "atomical_id": subject_atomical_id1, + "location_id": b"not_used", + "data": b"not_used", + "data_value": {"sat_value": 1000, "atomical_value": 1000}, + }, + { + "atomical_id": subject_atomical_id2, + "location_id": b"not_used", + "data": b"not_used", + "data_value": {"sat_value": 1000, "atomical_value": 1000}, + }, + ] + } + operation_found_at_inputs = parse_protocols_operations_from_witness_array(tx, tx_hash, True) + operation_found_at_inputs["op"] = "z" + operation_found_at_inputs["payload"] = { + subject_atomical_id1_compact: { + "0": 1200, # Flood the payload with more value than exists. + }, + subject_atomical_id2_compact: { + "1": 600, + "2": 600, # Flood the payload with more value than exists. + }, + } + + def mock_mint_fetcher(self, atomical_id): + return {"atomical_id": atomical_id, "type": "FT"} + + blueprint_builder = AtomicalsTransferBlueprintBuilder( + MockLogger(), + atomicals_spent_at_inputs, + operation_found_at_inputs, + tx_hash, + tx, + mock_mint_fetcher, + True, + True, + ) + nft_output_blueprint = blueprint_builder.get_nft_output_blueprint() + assert len(nft_output_blueprint.outputs) == 0 + ft_output_blueprint = blueprint_builder.get_ft_output_blueprint() + assert ft_output_blueprint.cleanly_assigned is False + assert len(ft_output_blueprint.outputs) == 3 + assert ft_output_blueprint.fts_burned == {} + assert blueprint_builder.get_are_fts_burned() is False + assert ft_output_blueprint.outputs[0]["atomicals"][subject_atomical_id1].atomical_value == 1000 + assert ft_output_blueprint.outputs[1]["atomicals"][subject_atomical_id2].atomical_value == 600 + assert ft_output_blueprint.outputs[2]["atomicals"][subject_atomical_id2].atomical_value == 400 + + def test_partially_colored_spends_are_payments_satisfied_checks(): raw_tx_str = "02000000000101647760b13086a2f2e77395e474305237afa65ec638dda01132c8c48c8b891fd00000000000ffffffff03a8610000000000002251208a586070907d75b89f1b7bcbe8dd5c623e0143e9b62d5d6759da06a59b749679a861000000000000225120ed2ec645d1749c9b2dba88b1346899c60c82f7a57e6359964393a2bba31450f200000000000000002d6a0461746f6d017024921bd27146f57d42565b373214ae7f6d05fa85c3f73eeb5dd876c4c81be58888000000000140d94db131ec889cb33fc258bc3bb5ace3656597cde88cf51494ae864f171915d262a50af24e3699560116450c4244a99b7d84602b8be1fe4c640250d2202330c800000000" raw_tx = bytes.fromhex(raw_tx_str)