2424_U64_FIELDS = ("id" , "merchant_id" )
2525_I128_FIELDS = ("amount" , "amount_paid" , "amount_refunded" )
2626_TIMESTAMP_FIELDS = ("date_created" , "date_paid" , "expires_at" )
27+ _U64_MAX = 2 ** 64 - 1
28+
29+
30+ def _validate_u64 (field : str , value : object ) -> int :
31+ """Coerce ``value`` to a strictly valid on-chain ``u64``.
32+
33+ Booleans and floats are rejected outright rather than coerced, since a
34+ silent bool->int or float->int cast could hide a malformed contract
35+ payload. Everything else is coerced via ``int()`` and range-checked
36+ against ``[0, 2**64 - 1]``.
37+ """
38+ if isinstance (value , bool ):
39+ raise ValueError (f"{ field } must be an integer, not a boolean" )
40+ if isinstance (value , float ):
41+ raise ValueError (f"{ field } must be an integer, not a float" )
42+ if isinstance (value , int ):
43+ result = value
44+ else :
45+ try :
46+ result = int (value )
47+ except (TypeError , ValueError ) as exc :
48+ raise ValueError (
49+ f"{ field } must be an integer, got { type (value ).__name__ } "
50+ ) from exc
51+ if not 0 <= result <= _U64_MAX :
52+ raise ValueError (
53+ f"{ field } must be between 0 and { _U64_MAX } (u64 range), got { result } "
54+ )
55+ return result
2756
2857
2958class InvoiceStatus (str , Enum ):
@@ -110,14 +139,20 @@ def _from_contract_dict(cls, data: dict) -> dict:
110139 (or ``None``) pass through untouched, so re-running a previously
111140 converted dict — as happens on a ``to_dict()`` -> ``from_dict()``
112141 round trip — is a no-op rather than a double conversion.
142+
143+ Raises:
144+ ValueError: If a ``u64`` or timestamp field is a boolean, a
145+ float, negative, or exceeds ``2**64 - 1``.
146+ OverflowError: If a timestamp is within the valid ``u64`` range
147+ but too large for :class:`~datetime.datetime` to represent.
113148 """
114149 converted = dict (data )
115150
116151 for field in _U64_FIELDS :
117152 value = converted .get (field )
118- if value is None or isinstance ( value , bool ) or isinstance ( value , int ) :
153+ if value is None :
119154 continue
120- converted [field ] = int ( value )
155+ converted [field ] = _validate_u64 ( field , value )
121156
122157 for field in _I128_FIELDS :
123158 value = converted .get (field )
@@ -129,9 +164,8 @@ def _from_contract_dict(cls, data: dict) -> dict:
129164 value = converted .get (field )
130165 if value is None or isinstance (value , datetime ):
131166 continue
132- if isinstance (value , bool ):
133- raise ValueError (f"{ field } must be a unix timestamp, not a boolean" )
134- converted [field ] = datetime .fromtimestamp (int (value ), tz = timezone .utc )
167+ timestamp = _validate_u64 (field , value )
168+ converted [field ] = datetime .fromtimestamp (timestamp , tz = timezone .utc )
135169
136170 return converted
137171
@@ -147,4 +181,10 @@ def from_dict(cls, data: dict) -> "Invoice":
147181 f"{ cls .__name__ } .from_dict() expects a dict, got "
148182 f"{ type (data ).__name__ } "
149183 )
150- return cls (** cls ._from_contract_dict (data ))
184+ try :
185+ converted = cls ._from_contract_dict (data )
186+ except (ValueError , OverflowError ) as err :
187+ field = str (err ).split (" " , 1 )[0 ]
188+ param = field if field in (* _U64_FIELDS , * _TIMESTAMP_FIELDS ) else None
189+ raise InvalidRequestError (str (err ), param = param ) from err
190+ return cls (** converted )
0 commit comments