Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decodes all Atomicals outputs with the transaction #233

Merged
merged 9 commits into from
Nov 21, 2024
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
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
2 changes: 1 addition & 1 deletion electrumx/server/block_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3529,7 +3529,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
29 changes: 19 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,13 @@ 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:
self.logger.error(f"Session request error: [method] {method}, [args] {args}, [error] {e}")
return RPCError(-1, str(e))
124 changes: 65 additions & 59 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 @@ -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 @@ -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 Down
8 changes: 4 additions & 4 deletions electrumx/server/session/shared_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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):
Expand Down
Loading