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

Fix payment and add testcase, support partially color payment #205

Merged
merged 2 commits into from
Jun 15, 2024
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
52 changes: 9 additions & 43 deletions electrumx/lib/atomicals_blueprint_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -785,12 +785,6 @@ def build_atomical_input_summaries(
# For each input atomical spent at the current input...
for atomicals_entry in atomicals_entry_list:
atomical_id = atomicals_entry["atomical_id"]
# value, = unpack_le_uint64(
# atomicals_entry['data'][HASHX_LEN + SCRIPTHASH_LEN : HASHX_LEN + SCRIPTHASH_LEN + 8]
# )
# exponent, = unpack_le_uint16_from(
# atomicals_entry['data'][HASHX_LEN + SCRIPTHASH_LEN + 8: HASHX_LEN + SCRIPTHASH_LEN + 8 + 8]
# )
sat_value = atomicals_entry["data_value"]["sat_value"]
atomical_value = atomicals_entry["data_value"]["atomical_value"]
# Perform a cache lookup for the mint information since we do not want to query multiple times
Expand Down Expand Up @@ -924,7 +918,7 @@ def get_atomical_id_for_payment_marker_if_found(cls, tx):

return found_atomical_id, None, None

def are_payments_satisfied(self, expected_payment_outputs, atomicals_spent_at_inputs):
def are_payments_satisfied(self, expected_payment_outputs):
if not isinstance(expected_payment_outputs, dict) or len(expected_payment_outputs.keys()) < 1:
return False

Expand Down Expand Up @@ -958,35 +952,6 @@ def are_payments_satisfied(self, expected_payment_outputs, atomicals_spent_at_in
# Map the output script hex only
expected_output_keys_satisfied[output_script_key] = False

# Prepare the mapping of which ARC20 is paid at which output
ft_coloring_summary = calculate_outputs_to_color_for_ft_atomical_ids(
self.tx,
self.ft_atomicals,
self.sort_fifo,
self.is_custom_coloring_activated,
)
output_idx_to_atomical_id_map = {}
if ft_coloring_summary:
output_idx_to_atomical_id_map = build_reverse_output_to_atomical_id_exponent_map(
ft_coloring_summary.atomical_id_to_expected_outs_map
)

atomicals_inputs_values = {}
for atomicals_input in atomicals_spent_at_inputs.values():
for atomical_entry in atomicals_input:
atomical_id = atomical_entry["atomical_id"]
sat_value = atomical_entry["data_value"]["sat_value"]
atomical_value = atomical_entry["data_value"]["atomical_value"]
if atomical_id not in atomicals_inputs_values:
atomicals_inputs_values[atomical_id] = {
"sat_value": 0,
"atomical_value": 0,
}
atomicals_inputs_values[atomical_id] = {
"sat_value": atomicals_inputs_values[atomical_id]["sat_value"] + sat_value,
"atomical_value": atomicals_inputs_values[atomical_id]["atomical_value"] + atomical_value,
}

# For each of the outputs, assess whether it matches any of the required payment output expectations
for idx, txout in enumerate(self.tx.outputs):
output_script_hex = txout.pk_script.hex()
Expand Down Expand Up @@ -1016,15 +981,16 @@ def are_payments_satisfied(self, expected_payment_outputs, atomicals_spent_at_in
expected_output_payment_id_type
)
# Check in the reverse map if the current output idx is colored with the expected color
output_summary = output_idx_to_atomical_id_map.get(idx)
if output_summary and output_summary.get(expected_output_payment_id_type_long_form, None) is not None:
output_summary = (
self.ft_output_blueprint.outputs.get(idx, {})
.get("atomicals", {})
.get(expected_output_payment_id_type_long_form, None)
)
if output_summary:
# Ensure the normalized atomical_value is greater than
# or equal to the expected payment amount in that token type.
# exponent_for_for_atomical_id = output_summary.get(expected_output_payment_id_type_long_form)
atomical_value = atomicals_inputs_values.get(expected_output_payment_id_type_long_form, {}).get(
"atomical_value", 0
)
if atomical_value >= (expected_output_payment_value or 0):
atomical_value = output_summary.atomical_value
if atomical_value >= expected_output_payment_value:
# Mark that the output was matched at least once
key = output_script_hex + expected_output_payment_id_type_long_form.hex()
expected_output_keys_satisfied[key] = True
Expand Down
110 changes: 95 additions & 15 deletions tests/lib/test_atomicals_blueprint_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -537,30 +537,30 @@ def mock_mint_fetcher(self, atomical_id):

# Empty rules
rules = {}
payment_valid = blueprint_builder.are_payments_satisfied(rules, atomicals_spent_at_inputs)
payment_valid = blueprint_builder.are_payments_satisfied(rules)
assert not payment_valid
# Valid payment to 2 outputs
rules = {
"51208a586070907d75b89f1b7bcbe8dd5c623e0143e9b62d5d6759da06a59b749679": {"v": 25000},
"5120ed2ec645d1749c9b2dba88b1346899c60c82f7a57e6359964393a2bba31450f2": {"v": 25000},
}
payment_valid = blueprint_builder.are_payments_satisfied(rules, atomicals_spent_at_inputs)
payment_valid = blueprint_builder.are_payments_satisfied(rules)
assert payment_valid
# Valid payment to one output
rules = {"51208a586070907d75b89f1b7bcbe8dd5c623e0143e9b62d5d6759da06a59b749679": {"v": 25000}}
payment_valid = blueprint_builder.are_payments_satisfied(rules, atomicals_spent_at_inputs)
payment_valid = blueprint_builder.are_payments_satisfied(rules)
assert payment_valid
# Invalid payment insufficient amount
rules = {"51208a586070907d75b89f1b7bcbe8dd5c623e0143e9b62d5d6759da06a59b749679": {"v": 25001}}
payment_valid = blueprint_builder.are_payments_satisfied(rules, atomicals_spent_at_inputs)
payment_valid = blueprint_builder.are_payments_satisfied(rules)
assert not payment_valid
# Valid payment higher amount
rules = {"51208a586070907d75b89f1b7bcbe8dd5c623e0143e9b62d5d6759da06a59b749679": {"v": 24999}}
payment_valid = blueprint_builder.are_payments_satisfied(rules, atomicals_spent_at_inputs)
payment_valid = blueprint_builder.are_payments_satisfied(rules)
assert payment_valid
# Invalid payment to wrong address
rules = {"51208a586070907d75b89f1b7bcbe8dd5c623e0143e9b62d5d6759da06a59b749678": {"v": 25000}}
payment_valid = blueprint_builder.are_payments_satisfied(rules, atomicals_spent_at_inputs)
payment_valid = blueprint_builder.are_payments_satisfied(rules)
assert not payment_valid


Expand Down Expand Up @@ -599,9 +599,6 @@ def mock_mint_fetcher(self, atomical_id):
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()
for k, v in ft_output_blueprint.outputs.items():
for i, ii in v["atomicals"].items():
print(ii.__dict__)
assert ft_output_blueprint.cleanly_assigned == True
assert blueprint_builder.get_are_fts_burned() == False

Expand All @@ -612,7 +609,7 @@ def mock_mint_fetcher(self, atomical_id):
"v": 25000,
}
}
payment_valid = blueprint_builder.are_payments_satisfied(rules, atomicals_spent_at_inputs)
payment_valid = blueprint_builder.are_payments_satisfied(rules)
assert not payment_valid

# Valid with a valid atomical id ft token
Expand All @@ -624,7 +621,7 @@ def mock_mint_fetcher(self, atomical_id):
}
}
#
payment_valid = blueprint_builder.are_payments_satisfied(rules, atomicals_spent_at_inputs)
payment_valid = blueprint_builder.are_payments_satisfied(rules)
assert payment_valid

# Invalid due to insufficient units
Expand All @@ -635,8 +632,8 @@ def mock_mint_fetcher(self, atomical_id):
"v": 25001,
}
}
payment_valid = blueprint_builder.are_payments_satisfied(rules, atomicals_spent_at_inputs)
assert payment_valid
payment_valid = blueprint_builder.are_payments_satisfied(rules)
assert not payment_valid
# Valid with a valid atomical id ft token higher than needed
subject_atomical_id_compact = location_id_bytes_to_compact(subject_atomical_id)
rules = {
Expand All @@ -646,7 +643,7 @@ def mock_mint_fetcher(self, atomical_id):
}
}
#
payment_valid = blueprint_builder.are_payments_satisfied(rules, atomicals_spent_at_inputs)
payment_valid = blueprint_builder.are_payments_satisfied(rules)
assert payment_valid

# Valid with a valid atomical id ft token higher than needed
Expand All @@ -657,7 +654,7 @@ def mock_mint_fetcher(self, atomical_id):
"v": 0,
}
}
payment_valid = blueprint_builder.are_payments_satisfied(rules, atomicals_spent_at_inputs)
payment_valid = blueprint_builder.are_payments_satisfied(rules)
assert payment_valid


Expand Down Expand Up @@ -1693,3 +1690,86 @@ def mock_mint_fetcher(self, atomical_id):
assert len(ft_output_blueprint.outputs) == 0
assert ft_output_blueprint.fts_burned == {}
assert blueprint_builder.get_are_fts_burned() == False


def test_partially_colored_spends_are_payments_satisfied_checks():
raw_tx_str = "02000000000101647760b13086a2f2e77395e474305237afa65ec638dda01132c8c48c8b891fd00000000000ffffffff03a8610000000000002251208a586070907d75b89f1b7bcbe8dd5c623e0143e9b62d5d6759da06a59b749679a861000000000000225120ed2ec645d1749c9b2dba88b1346899c60c82f7a57e6359964393a2bba31450f200000000000000002d6a0461746f6d017024921bd27146f57d42565b373214ae7f6d05fa85c3f73eeb5dd876c4c81be58888000000000140d94db131ec889cb33fc258bc3bb5ace3656597cde88cf51494ae864f171915d262a50af24e3699560116450c4244a99b7d84602b8be1fe4c640250d2202330c800000000"
raw_tx = bytes.fromhex(raw_tx_str)
subject_atomical_id = (
b"A\x03\x8f'\xe7\x85`l\xa0\xcc\x1e\xfd\x8e:\xa9\x12\xa1\\r\xd0o5\x9a\xeb\x05$=\xab+p\xa8V\x01\x00\x00\x00"
)

tx, tx_hash = coin.DESERIALIZER(raw_tx, 0).read_tx_and_hash()
atomicals_spent_at_inputs = {
1: [
{
"atomical_id": subject_atomical_id,
"location_id": b"not_used",
"data": b"not_used",
"data_value": {"sat_value": 50000, "atomical_value": 2},
},
],
}

def mock_mint_fetcher(self, atomical_id):
return {"atomical_id": atomical_id, "type": "FT"}

operations_at_inputs = {}
blueprint_builder = AtomicalsTransferBlueprintBuilder(
MockLogger(),
atomicals_spent_at_inputs,
operations_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 len(ft_output_blueprint.outputs) == 1
assert ft_output_blueprint.cleanly_assigned == False
assert blueprint_builder.get_are_fts_burned() == False

subject_atomical_id_compact = location_id_bytes_to_compact(subject_atomical_id)
# Empty rules
rules = {}
payment_valid = blueprint_builder.are_payments_satisfied(rules)
assert not payment_valid
# Valid payment to one output
rules = {
"51208a586070907d75b89f1b7bcbe8dd5c623e0143e9b62d5d6759da06a59b749679": {
"id": subject_atomical_id_compact,
"v": 2,
}
}
payment_valid = blueprint_builder.are_payments_satisfied(rules)
assert payment_valid
# Invalid payment insufficient amount
rules = {
"51208a586070907d75b89f1b7bcbe8dd5c623e0143e9b62d5d6759da06a59b749679": {
"id": subject_atomical_id_compact,
"v": 3,
}
}
payment_valid = blueprint_builder.are_payments_satisfied(rules)
assert not payment_valid
# Valid payment higher amount
rules = {
"51208a586070907d75b89f1b7bcbe8dd5c623e0143e9b62d5d6759da06a59b749679": {
"id": subject_atomical_id_compact,
"v": 1,
}
}
payment_valid = blueprint_builder.are_payments_satisfied(rules)
assert payment_valid
# Invalid payment to wrong address
rules = {
"51208a586070907d75b89f1b7bcbe8dd5c623e0143e9b62d5d6759da06a59b749678": {
"id": subject_atomical_id_compact,
"v": 2,
}
}
payment_valid = blueprint_builder.are_payments_satisfied(rules)
assert not payment_valid
Loading