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

Python: rework OutputId so it works in deserialization #2048

Merged
merged 4 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
10 changes: 5 additions & 5 deletions bindings/python/iota_sdk/client/_node_indexer_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ def account_output_id(self, account_id: HexStr) -> OutputId:
Returns:
The output ID of the account output.
"""
return OutputId.from_string(self._call_method('accountOutputId', {
return OutputId(self._call_method('accountOutputId', {
'accountId': account_id
}))

Expand All @@ -302,7 +302,7 @@ def anchor_output_id(self, anchor_id: HexStr) -> OutputId:
Returns:
The output ID of the anchor output.
"""
return OutputId.from_string(self._call_method('anchorOutputId', {
return OutputId(self._call_method('anchorOutputId', {
'anchorId': anchor_id
}))

Expand All @@ -325,7 +325,7 @@ def delegation_output_id(self, delegation_id: HexStr) -> OutputId:
Returns:
The output ID of the delegation output.
"""
return OutputId.from_string(self._call_method('delegationOutputId', {
return OutputId(self._call_method('delegationOutputId', {
'delegationId': delegation_id
}))

Expand All @@ -348,7 +348,7 @@ def foundry_output_id(self, foundry_id: HexStr) -> OutputId:
Returns:
The output ID of the foundry output.
"""
return OutputId.from_string(self._call_method('foundryOutputId', {
return OutputId(self._call_method('foundryOutputId', {
'foundryId': foundry_id
}))

Expand All @@ -371,6 +371,6 @@ def nft_output_id(self, nft_id: HexStr) -> OutputId:
Returns:
The output ID of the NFT output.
"""
return OutputId.from_string(self._call_method('nftOutputId', {
return OutputId(self._call_method('nftOutputId', {
'nftId': nft_id
}))
2 changes: 1 addition & 1 deletion bindings/python/iota_sdk/client/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def __init__(self, output_dict: Dict):
self.committed_slot = output_dict["committedSlot"]
self.page_size = output_dict["pageSize"]
self.cursor = output_dict["cursor"]
self.items = [OutputId.from_string(
self.items = [OutputId(
output_id) for output_id in output_dict["items"]]


Expand Down
68 changes: 35 additions & 33 deletions bindings/python/iota_sdk/types/output_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,64 +2,66 @@
# SPDX-License-Identifier: Apache-2.0

from dataclasses import dataclass

from iota_sdk.types.common import HexStr, json
from iota_sdk.types.common import json
from iota_sdk.types.output import Output
from iota_sdk.types.transaction_id import TransactionId


class OutputId(dict):
class OutputId(str):
"""Represents an output ID.

Attributes:
output_id: The unique id of an output.
transaction_id: The transaction id associated with the output.
output_index: The index of the output within a transaction.

"""

def __init__(self, transaction_id: TransactionId, output_index: int):
def __new__(cls, output_id: str):
"""Initialize OutputId
"""
if len(transaction_id) != 74:
if len(output_id) != 78:
raise ValueError(
'transaction_id length must be 74 characters with 0x prefix')
if not transaction_id.startswith('0x'):
raise ValueError('transaction_id must start with 0x')
# Validate that it has only valid hex characters
int(transaction_id[2:], 16)
output_index_hex = (output_index).to_bytes(2, "little").hex()
self.output_id = transaction_id + output_index_hex
self.transaction_id = transaction_id
self.output_index = output_index
'output_id length must be 78 characters with 0x prefix')
if not output_id.startswith('0x'):
raise ValueError('output_id must start with 0x')
instance = super().__new__(cls, output_id)
thibault-martinez marked this conversation as resolved.
Show resolved Hide resolved
return instance

@classmethod
def from_string(cls, output_id: HexStr):
"""Creates an `OutputId` instance from a `HexStr`.
def from_transaction_id_and_output_index(cls, transaction_id: TransactionId, output_index: int):
"""Creates an `OutputId` instance from its transaction id and output index.

Args:
output_id: The unique id of an output as a hex string.
transaction_id: The transaction id associated with the output.
output_index: The index of the output within a transaction.

Returns:
OutputId: The unique id of an output.
"""
obj = cls.__new__(cls)
super(OutputId, obj).__init__()
if len(output_id) != 78:
if len(transaction_id) != 74:
raise ValueError(
'output_id length must be 78 characters with 0x prefix')
if not output_id.startswith('0x'):
'transaction_id length must be 74 characters with 0x prefix')
if not transaction_id.startswith('0x'):
raise ValueError('transaction_id must start with 0x')
# Validate that it has only valid hex characters
int(output_id[2:], 16)
obj.output_id = output_id
obj.transaction_id = TransactionId(output_id[:74])
obj.output_index = int.from_bytes(
bytes.fromhex(output_id[74:]), 'little')
return obj
int(transaction_id[2:], 16)
output_index_hex = (output_index).to_bytes(2, "little").hex()
return OutputId(transaction_id + output_index_hex)

def __repr__(self):
return self.output_id
def transaction_id(self) -> TransactionId:
"""Returns the TransactionId of an OutputId.
"""
return TransactionId(self[:74])

def output_index(self) -> int:
"""Returns the output index of an OutputId.
"""
return int.from_bytes(
bytes.fromhex(self[74:]), 'little')

@classmethod
def from_dict(cls, output_id_dict: dict):
"""Init an OutputId from a dict.
"""
return OutputId(output_id_dict)


@json
Expand Down
2 changes: 1 addition & 1 deletion bindings/python/iota_sdk/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def compute_output_id(transaction_id: TransactionId,
index: int) -> OutputId:
"""Compute the output id from transaction id and output index.
"""
return OutputId.from_string(_call_method('computeOutputId', {
return OutputId(_call_method('computeOutputId', {
'id': transaction_id,
'index': index,
}))
Expand Down
20 changes: 9 additions & 11 deletions bindings/python/tests/test_api_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from typing import Generic, TypeVar
from json import load, loads, dumps
from iota_sdk import RoutesResponse, CongestionResponse, ManaRewardsResponse, ValidatorResponse, CommitteeResponse, IssuanceBlockHeaderResponse, Block, OutputResponse, SlotCommitment
from iota_sdk import RoutesResponse, CongestionResponse, ManaRewardsResponse, ValidatorResponse, CommitteeResponse, IssuanceBlockHeaderResponse, Block, OutputMetadata, OutputResponse, SlotCommitment, UtxoChangesResponse, UtxoChangesFullResponse


base_path = '../../sdk/tests/types/api/fixtures/'
Expand Down Expand Up @@ -69,11 +69,10 @@ def test_api_response(cls_type: Generic[T], path: str):
test_api_response(
OutputResponse, "get-outputs-by-id-response-example.json")
# GET /api/core/v3/outputs/{outputId}/metadata
# TODO: enable when https://github.com/iotaledger/iota-sdk/issues/2020 is fixed
# test_api_response(
# OutputMetadata, "get-output-metadata-by-id-response-unspent-example.json")
# test_api_response(
# OutputMetadata, "get-output-metadata-by-id-response-spent-example.json")
test_api_response(
OutputMetadata, "get-output-metadata-by-id-response-unspent-example.json")
test_api_response(
OutputMetadata, "get-output-metadata-by-id-response-spent-example.json")
# GET /api/core/v3/outputs/{outputId}/full
# TODO: enable when OutputWithMetadata is updated with OutputIdProof https://github.com/iotaledger/iota-sdk/issues/2021
# test_api_response(OutputWithMetadata,
Expand All @@ -85,9 +84,8 @@ def test_api_response(cls_type: Generic[T], path: str):
# GET /api/core/v3/commitments/{commitmentId}
test_api_response(SlotCommitment, "get-commitment-response-example.json")
# GET /api/core/v3/commitments/{commitmentId}/utxo-changes
# TODO: enable when https://github.com/iotaledger/iota-sdk/issues/2020 is fixed
# test_api_response(UtxoChangesResponse,
# "get-utxo-changes-response-example.json")
test_api_response(UtxoChangesResponse,
"get-utxo-changes-response-example.json")
# GET /api/core/v3/commitments/{commitmentId}/utxo-changes/full
# test_api_response(UtxoChangesFullResponse,
# "get-utxo-changes-full-response-example.json")
test_api_response(UtxoChangesFullResponse,
"get-utxo-changes-full-response-example.json")
32 changes: 18 additions & 14 deletions bindings/python/tests/test_offline.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,35 +60,39 @@ def test_output_id(self):
transaction_id = TransactionId(
'0x52fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64900000000')
output_index = 42
output_id = OutputId(transaction_id, output_index)
assert repr(
output_id) == '0x52fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c649000000002a00'
output_id = OutputId.from_transaction_id_and_output_index(
transaction_id, output_index)
assert str(output_id
) == "0x52fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c649000000002a00" + ""

new_output_id = OutputId.from_string(
new_output_id = OutputId(
'0x52fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c649000000002a00')
assert repr(
new_output_id) == '0x52fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c649000000002a00'
assert new_output_id.transaction_id == transaction_id
assert new_output_id.output_index == output_index
assert str(new_output_id
) == '0x52fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c649000000002a00'
assert new_output_id.transaction_id() == transaction_id
assert new_output_id.output_index() == output_index

transaction_id_missing_0x_prefix = '52fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64900000000'
with self.assertRaises(ValueError):
OutputId(transaction_id_missing_0x_prefix, output_index)
OutputId.from_transaction_id_and_output_index(
transaction_id_missing_0x_prefix, output_index)
transaction_id__invalid_hex_prefix = '0052fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64900000000'
with self.assertRaises(ValueError):
OutputId(transaction_id__invalid_hex_prefix, output_index)
OutputId.from_transaction_id_and_output_index(
transaction_id__invalid_hex_prefix, output_index)
transaction_id_invalid_hex_char = '0xz2fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c64900000000'
with self.assertRaises(ValueError):
OutputId(transaction_id_invalid_hex_char, output_index)
OutputId.from_transaction_id_and_output_index(
transaction_id_invalid_hex_char, output_index)
output_id_missing_0x_prefix = '52fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c6492a000000000000'
with self.assertRaises(ValueError):
OutputId.from_string(output_id_missing_0x_prefix)
OutputId(output_id_missing_0x_prefix)
output_id_invalid_hex_char = '0xz2fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c6492a000000000000'
with self.assertRaises(ValueError):
OutputId.from_string(output_id_invalid_hex_char)
OutputId(output_id_invalid_hex_char)
output_id_invalid_hex_prefix = '0052fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c6492a000000000000'
with self.assertRaises(ValueError):
OutputId.from_string(output_id_invalid_hex_prefix)
OutputId(output_id_invalid_hex_prefix)


def test_hex_utf8():
Expand Down
Loading