Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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') }}
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ docs/_build
.idea/
.env
.venv
*.log
*.http
3 changes: 3 additions & 0 deletions electrumx/lib/atomicals_blueprint_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 13 additions & 6 deletions electrumx/lib/coins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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


Expand Down
8 changes: 8 additions & 0 deletions electrumx/lib/util_atomicals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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):
Expand Down
4 changes: 1 addition & 3 deletions electrumx/server/block_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions electrumx/server/session/http_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
31 changes: 21 additions & 10 deletions electrumx/server/session/session_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
NewlineFramer,
ReplyAndDisconnect,
Request,
RPCError,
RPCSession,
handler_invocation,
)
Expand Down Expand Up @@ -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
Expand All @@ -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))
130 changes: 68 additions & 62 deletions electrumx/server/session/session_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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:
Expand All @@ -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,
Expand All @@ -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

Expand Down
Loading