Skip to content

Commit 8df2374

Browse files
Merge pull request #243 from atomicals/develop
Minor release v1.5.2.0
2 parents 05ac927 + c5fd6cc commit 8df2374

13 files changed

+227
-90
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
python-version: '3.10'
2424
cache: 'pip'
2525
- name: Setup Python caches
26-
uses: actions/cache@v2
26+
uses: actions/cache@v4
2727
with:
2828
path: ${{ env.pythonLocation }}
2929
key: ${{ env.pythonLocation }}-${{ hashFiles('setup.py','requirements.txt','requirements-test.txt') }}

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@ docs/_build
1515
.idea/
1616
.env
1717
.venv
18+
*.log
19+
*.http

electrumx/lib/atomicals_blueprint_builder.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,9 @@ def custom_color_ft_atomicals(cls, ft_atomicals, operations_found_at_inputs, tx)
616616
# expected_value will equal to txout.value
617617
if expected_value > txout.value:
618618
expected_value = txout.value
619+
# The coloring value cannot exceed the remaining atomical value.
620+
if expected_value > remaining_value:
621+
expected_value = remaining_value
619622
# set cleanly_assigned
620623
if expected_value < txout.value:
621624
cleanly_assigned = False

electrumx/lib/coins.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -992,14 +992,21 @@ def warn_old_client_on_tx_broadcast(cls, client_ver):
992992
return False
993993

994994

995+
class BitcoinSegwitTestnet(BitcoinTestnet):
996+
NAME = "BitcoinSegwit" # support legacy name
997+
998+
995999
class BitcoinTestnet4(BitcoinTestnetMixin, AtomicalsCoinMixin, Coin):
9961000
NAME = "Bitcoin"
9971001
NET = "testnet4"
998-
DESERIALIZER = lib_tx.DeserializerSegWit
999-
CRASH_CLIENT_VER = (3, 2, 3)
1000-
PEERS = []
1001-
GENESIS_HASH = "00000000da84f2bafbbc53dee25a72ae" "507ff4914b867c565be350b0da8bf043"
1002-
RPC_PORT = 48332
1002+
PEERS = [
1003+
'blackie.c3-soft.com s57010 t57009',
1004+
'testnet4-electrumx.wakiyamap.dev',
1005+
]
1006+
GENESIS_HASH = ('00000000da84f2bafbbc53dee25a72ae'
1007+
'507ff4914b867c565be350b0da8bf043')
1008+
TX_COUNT = 1
1009+
TX_COUNT_HEIGHT = 1
10031010

10041011
ATOMICALS_ACTIVATION_HEIGHT = 27000
10051012
ATOMICALS_ACTIVATION_HEIGHT_DMINT = 27000
@@ -1022,7 +1029,7 @@ def warn_old_client_on_tx_broadcast(cls, client_ver):
10221029
return False
10231030

10241031

1025-
class BitcoinSegwitTestnet(BitcoinTestnet):
1032+
class BitcoinSegwitTestnet4(BitcoinTestnet4):
10261033
NAME = "BitcoinSegwit" # support legacy name
10271034

10281035

electrumx/lib/util_atomicals.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1450,6 +1450,10 @@ def auto_encode_bytes_elements(state):
14501450
for key, value in state.items():
14511451
state[key] = auto_encode_bytes_elements(value)
14521452

1453+
# Handles unknown undefined type.
1454+
if type(state).__name__ == 'undefined_type':
1455+
return None
1456+
14531457
return state
14541458

14551459

@@ -1468,6 +1472,10 @@ def auto_encode_bytes_items(state):
14681472
reformatted_list.append(auto_encode_bytes_elements(item))
14691473
return reformatted_list
14701474

1475+
# Handles unknown undefined type.
1476+
if type(state).__name__ == 'undefined_type':
1477+
return None
1478+
14711479
cloned_state = {}
14721480
try:
14731481
if isinstance(state, dict):

electrumx/server/block_processor.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -580,8 +580,6 @@ def get_general_data_with_cache(self, key):
580580
cache = self.general_data_cache.get(key)
581581
if not cache:
582582
cache = self.db.get_general_data(key)
583-
if cache:
584-
self.general_data_cache[key] = cache
585583
return cache
586584

587585
# 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):
35293527

35303528
# Builds a map of the atomicals spent at a tx
35313529
# It uses the spend_atomicals_utxo method but with live_run == False
3532-
def build_atomicals_receive_at_ouutput_for_validation_only(self, tx, txid):
3530+
def build_atomicals_receive_at_output_for_validation_only(self, tx, txid):
35333531
spend_atomicals_utxo = self.spend_atomicals_utxo
35343532
atomicals_receive_at_outputs = {}
35353533
txout_index = 0

electrumx/server/session/http_session.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,10 +241,10 @@ async def donation_address(self):
241241
"""Return the donation address as a string, empty if there is none."""
242242
return self.env.donation_address
243243

244-
async def server_features_async(self):
244+
def server_features_async(self):
245245
return self.server_features(self.env)
246246

247-
async def peers_subscribe(self):
247+
def peers_subscribe(self):
248248
"""Return the server peers as a list of (ip, host, details) tuples."""
249249
return self.peer_mgr.on_peers_subscribe(False)
250250

electrumx/server/session/session_base.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
NewlineFramer,
88
ReplyAndDisconnect,
99
Request,
10+
RPCError,
1011
RPCSession,
1112
handler_invocation,
1213
)
@@ -119,14 +120,17 @@ async def handle_request(self, request):
119120
"""Handle an incoming request. ElectrumX doesn't receive
120121
notifications from client sessions.
121122
"""
123+
method = request.method
122124
if isinstance(request, Request):
123-
handler = self.request_handlers.get(request.method)
124-
method = request.method
125-
args = request.args
125+
handler = self.request_handlers.get(method)
126126
else:
127127
handler = None
128-
method = "invalid method"
129-
args = None
128+
if handler is None:
129+
from aiorpcx import JSONRPC
130+
self.logger.error(f'Unknown handler for the method "{method}"')
131+
return RPCError(JSONRPC.METHOD_NOT_FOUND, f'Unknown handler for the method "{method}"')
132+
133+
args = request.args
130134
self.logger.debug(f"Session request handling: [method] {method}, [args] {args}")
131135

132136
# If DROP_CLIENT_UNKNOWN is enabled, check if the client identified
@@ -136,8 +140,15 @@ async def handle_request(self, request):
136140
raise ReplyAndDisconnect(BAD_REQUEST, "use server.version to identify client")
137141

138142
self.session_mgr.method_counts[method] += 1
139-
coro = handler_invocation(handler, request)()
140-
if isinstance(coro, Awaitable):
141-
return await coro
142-
else:
143-
return coro
143+
144+
# Wraps all internal errors without closing the session.
145+
try:
146+
result = handler_invocation(handler, request)()
147+
if isinstance(result, Awaitable):
148+
result = await result
149+
return result
150+
except BaseException as e:
151+
import traceback
152+
stack = traceback.format_exc()
153+
self.logger.error(f"Session request error: [method:{method}], [args:{args}], [error:{e}], [stack:{stack}]")
154+
return RPCError(-1, str(e))

electrumx/server/session/session_manager.py

Lines changed: 68 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from collections import defaultdict
88
from functools import partial
99
from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network
10-
from typing import TYPE_CHECKING, Dict, List, Optional
10+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
1111

1212
import attr
1313
import pylru
@@ -47,7 +47,7 @@
4747
from electrumx.server.session import BAD_REQUEST, DAEMON_ERROR
4848
from electrumx.server.session.http_session import HttpSession
4949
from electrumx.server.session.rpc_session import LocalRPC
50-
from electrumx.server.session.util import SESSION_PROTOCOL_MAX, non_negative_integer
50+
from electrumx.server.session.util import SESSION_PROTOCOL_MAX, assert_tx_hash, non_negative_integer
5151
from electrumx.version import electrumx_version
5252

5353
if TYPE_CHECKING:
@@ -953,7 +953,7 @@ async def transaction_decode_raw_tx_blueprint(
953953
},
954954
}
955955
elif op == "nft":
956-
_receive_at_outputs = self.bp.build_atomicals_receive_at_ouutput_for_validation_only(tx, tx_hash)
956+
_receive_at_outputs = self.bp.build_atomicals_receive_at_output_for_validation_only(tx, tx_hash)
957957
tx_out = tx.outputs[0]
958958
atomical_id = location_id_bytes_to_compact(_receive_at_outputs[0][-1]["atomical_id"])
959959
mint_info = {
@@ -991,7 +991,7 @@ async def transaction_decode_raw_tx_blueprint(
991991
# Analysis the transaction detail by txid.
992992
# See BlockProcessor.op_list for the complete op list.
993993
async def get_transaction_detail(self, tx_id: str, height=None, tx_num=-1):
994-
tx_hash = hex_str_to_hash(tx_id)
994+
tx_hash = assert_tx_hash(tx_id)
995995
res = self._tx_detail_cache.get(tx_hash)
996996
if res:
997997
# 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):
10111011

10121012
operation_found_at_inputs = parse_protocols_operations_from_witness_array(tx, tx_hash, True)
10131013
atomicals_spent_at_inputs = self.bp.build_atomicals_spent_at_inputs_for_validation_only(tx)
1014-
atomicals_receive_at_outputs = self.bp.build_atomicals_receive_at_ouutput_for_validation_only(tx, tx_hash)
1014+
atomicals_receive_at_outputs = self.bp.build_atomicals_receive_at_output_for_validation_only(tx, tx_hash)
10151015
blueprint_builder = AtomicalsTransferBlueprintBuilder(
10161016
self.logger,
10171017
atomicals_spent_at_inputs,
@@ -1043,62 +1043,6 @@ async def get_transaction_detail(self, tx_id: str, height=None, tx_num=-1):
10431043
"is_cleanly_assigned": is_cleanly_assigned,
10441044
},
10451045
}
1046-
operation_type = operation_found_at_inputs.get("op", "") if operation_found_at_inputs else ""
1047-
if operation_found_at_inputs:
1048-
payload = operation_found_at_inputs.get("payload")
1049-
payload_not_none = payload or {}
1050-
res["info"]["payload"] = payload_not_none
1051-
if blueprint_builder.is_mint and operation_type in ["dmt", "ft"]:
1052-
expected_output_index = 0
1053-
tx_out = tx.outputs[expected_output_index]
1054-
location = tx_hash + util.pack_le_uint32(expected_output_index)
1055-
# if save into the db, it means mint success
1056-
has_atomicals = self.db.get_atomicals_by_location_long_form(location)
1057-
if len(has_atomicals):
1058-
ticker_name = payload_not_none.get("args", {}).get("mint_ticker", "")
1059-
status, candidate_atomical_id, _ = self.bp.get_effective_ticker(ticker_name, self.bp.height)
1060-
if status:
1061-
atomical_id = location_id_bytes_to_compact(candidate_atomical_id)
1062-
res["info"] = {
1063-
"atomical_id": atomical_id,
1064-
"location_id": location_id_bytes_to_compact(location),
1065-
"payload": payload,
1066-
"outputs": {
1067-
expected_output_index: [
1068-
{
1069-
"address": get_address_from_output_script(tx_out.pk_script),
1070-
"atomical_id": atomical_id,
1071-
"type": "FT",
1072-
"index": expected_output_index,
1073-
"value": tx_out.value,
1074-
}
1075-
]
1076-
},
1077-
}
1078-
elif operation_type == "nft":
1079-
if atomicals_receive_at_outputs:
1080-
expected_output_index = 0
1081-
location = tx_hash + util.pack_le_uint32(expected_output_index)
1082-
tx_out = tx.outputs[expected_output_index]
1083-
atomical_id = location_id_bytes_to_compact(
1084-
atomicals_receive_at_outputs[expected_output_index][-1]["atomical_id"]
1085-
)
1086-
res["info"] = {
1087-
"atomical_id": atomical_id,
1088-
"location_id": location_id_bytes_to_compact(location),
1089-
"payload": payload,
1090-
"outputs": {
1091-
expected_output_index: [
1092-
{
1093-
"address": get_address_from_output_script(tx_out.pk_script),
1094-
"atomical_id": atomical_id,
1095-
"type": "NFT",
1096-
"index": expected_output_index,
1097-
"value": tx_out.value,
1098-
}
1099-
]
1100-
},
1101-
}
11021046

11031047
async def make_transfer_inputs(result, inputs_atomicals, tx_inputs, make_type) -> Dict[int, List[Dict]]:
11041048
for atomical_id, input_data in inputs_atomicals.items():
@@ -1146,6 +1090,8 @@ def make_transfer_outputs(
11461090
result[k].append(_data)
11471091
return result
11481092

1093+
operation_type = operation_found_at_inputs.get("op", "") if operation_found_at_inputs else ""
1094+
11491095
# no operation_found_at_inputs, it will be transfer.
11501096
if blueprint_builder.ft_atomicals and atomicals_spent_at_inputs:
11511097
if not operation_type and not op_raw:
@@ -1158,6 +1104,66 @@ def make_transfer_outputs(
11581104
await make_transfer_inputs(res["transfers"]["inputs"], blueprint_builder.nft_atomicals, tx.inputs, "NFT")
11591105
make_transfer_outputs(res["transfers"]["outputs"], blueprint_builder.nft_output_blueprint.outputs)
11601106

1107+
if operation_found_at_inputs:
1108+
payload = operation_found_at_inputs.get("payload")
1109+
payload_not_none = payload or {}
1110+
res["info"]["payload"] = payload_not_none
1111+
# Mint operation types are "dmt", "nft", "ft", "dft". "dft" is the deploy operation.
1112+
if operation_type in ["dmt", "ft"]:
1113+
expected_output_index = 0
1114+
tx_out = tx.outputs[expected_output_index]
1115+
location = tx_hash + util.pack_le_uint32(expected_output_index)
1116+
# if save into the db, it means mint success
1117+
has_atomicals = self.db.get_atomicals_by_location_long_form(location)
1118+
if len(has_atomicals):
1119+
ticker_name = payload_not_none.get("args", {}).get("mint_ticker", "")
1120+
status, candidate_atomical_id, _ = self.bp.get_effective_ticker(ticker_name, self.bp.height)
1121+
if status:
1122+
atomical_id = location_id_bytes_to_compact(candidate_atomical_id)
1123+
res["info"] = {
1124+
"payload": payload,
1125+
"outputs": {
1126+
expected_output_index: [
1127+
{
1128+
"address": get_address_from_output_script(tx_out.pk_script),
1129+
"atomical_id": atomical_id,
1130+
"location_id": location_id_bytes_to_compact(location),
1131+
"type": "FT",
1132+
"index": expected_output_index,
1133+
"value": tx_out.value,
1134+
}
1135+
]
1136+
},
1137+
}
1138+
elif operation_type == "nft":
1139+
if atomicals_receive_at_outputs:
1140+
outputs: Dict[int, List[Dict[str, Any]]] = {}
1141+
for expected_output_index, atomicals_receives in atomicals_receive_at_outputs.items():
1142+
receives: List[Dict[str, Any]] = []
1143+
for atomicals in atomicals_receives:
1144+
atomical_id = location_id_bytes_to_compact(atomicals["atomical_id"])
1145+
if any(
1146+
any(output.get("atomical_id") == atomical_id for output in outputs_list)
1147+
for outputs_list in res["transfers"]["outputs"].values()
1148+
):
1149+
continue
1150+
location = tx_hash + util.pack_le_uint32(expected_output_index)
1151+
tx_out = tx.outputs[expected_output_index]
1152+
receives.append({
1153+
"address": get_address_from_output_script(tx_out.pk_script),
1154+
"atomical_id": atomical_id,
1155+
"location_id": location_id_bytes_to_compact(location),
1156+
"type": "NFT",
1157+
"index": expected_output_index,
1158+
"value": tx_out.value,
1159+
})
1160+
if len(receives) > 0:
1161+
outputs[expected_output_index] = receives
1162+
res["info"] = {
1163+
"payload": payload,
1164+
"outputs": outputs,
1165+
}
1166+
11611167
(
11621168
payment_id,
11631169
payment_marker_idx,
@@ -1177,7 +1183,7 @@ def make_transfer_outputs(
11771183
return auto_encode_bytes_elements(res)
11781184

11791185
async def get_transaction_detail_batch(self, tx_ids: str):
1180-
tasks = [self.get_transaction_detail(txid) for txid in tx_ids.split(',')]
1186+
tasks = [self.get_transaction_detail(assert_tx_hash(tx_id)) for tx_id in tx_ids.split(',')]
11811187
details = await asyncio.gather(*tasks)
11821188
return details
11831189

0 commit comments

Comments
 (0)