From 45865e1b573e3d5e0e42bfe1bcd3038513b82f52 Mon Sep 17 00:00:00 2001 From: Valery Date: Thu, 17 Sep 2026 20:02:02 +0100 Subject: [PATCH] feat(api): support native asset metadata --- docs/Asset.md | 5 ++++- lighter/models/asset.py | 24 ++++++++++++++++++++---- openapi.json | 26 ++++++++++++++++++++++++-- test/test_asset_metadata.py | 35 +++++++++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 test/test_asset_metadata.py diff --git a/docs/Asset.md b/docs/Asset.md index be7c42c1..731b5094 100644 --- a/docs/Asset.md +++ b/docs/Asset.md @@ -7,7 +7,10 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **asset_id** | **int** | | **symbol** | **str** | | -**l1_decimals** | **int** | | +**l1_decimals** | **int** | L1 token decimals when supplied by registration. Omitted for Core-native and EVM-native assets. | [optional] +**evm_decimals** | **int** | Canonical EVM token decimals for EVM-native assets. Zero is a valid precision; omission means unavailable. | [optional] +**canonical_domain** | **int** | Asset origin: 0 legacy, 1 L1, 2 Core, 3 EVM. Omitted by older API versions. | [optional] +**evm_token** | **str** | Linked EVM token or wrapper address. The zero address means unlinked; older API versions omit this field. | [optional] **decimals** | **int** | | **min_transfer_amount** | **str** | | **min_withdrawal_amount** | **str** | | diff --git a/lighter/models/asset.py b/lighter/models/asset.py index 46f70d1f..73ba0b89 100644 --- a/lighter/models/asset.py +++ b/lighter/models/asset.py @@ -17,8 +17,8 @@ import re # noqa: F401 import json -from pydantic import BaseModel, ConfigDict, StrictInt, StrictStr, field_validator -from typing import Any, ClassVar, Dict, List +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List, Optional from typing import Optional, Set from typing_extensions import Self @@ -28,7 +28,10 @@ class Asset(BaseModel): """ # noqa: E501 asset_id: StrictInt symbol: StrictStr - l1_decimals: StrictInt + l1_decimals: Optional[StrictInt] = Field(default=None, description="L1 token decimals when supplied by registration. Omitted for Core-native and EVM-native assets.") + evm_decimals: Optional[StrictInt] = Field(default=None, description="Canonical EVM token decimals for EVM-native assets. Zero is a valid precision; omission means unavailable.") + canonical_domain: Optional[StrictInt] = Field(default=None, description="Asset origin: 0 legacy, 1 L1, 2 Core, 3 EVM. Omitted by older API versions.") + evm_token: Optional[StrictStr] = Field(default=None, description="Linked EVM token or wrapper address. The zero address means unlinked; older API versions omit this field.") decimals: StrictInt min_transfer_amount: StrictStr min_withdrawal_amount: StrictStr @@ -45,7 +48,17 @@ class Asset(BaseModel): liquidation_factor: StrictStr multiplier: StrictStr additional_properties: Dict[str, Any] = {} - __properties: ClassVar[List[str]] = ["asset_id", "symbol", "l1_decimals", "decimals", "min_transfer_amount", "min_withdrawal_amount", "margin_mode", "index_price", "l1_address", "global_supply_cap", "liquidation_fee", "liquidation_threshold", "loan_to_value", "price_decimals", "total_supplied", "user_supply_cap", "liquidation_factor", "multiplier"] + __properties: ClassVar[List[str]] = ["asset_id", "symbol", "l1_decimals", "evm_decimals", "canonical_domain", "evm_token", "decimals", "min_transfer_amount", "min_withdrawal_amount", "margin_mode", "index_price", "l1_address", "global_supply_cap", "liquidation_fee", "liquidation_threshold", "loan_to_value", "price_decimals", "total_supplied", "user_supply_cap", "liquidation_factor", "multiplier"] + + @field_validator('canonical_domain') + def canonical_domain_validate_enum(cls, value): + """Validates the enum""" + if value is None: + return value + + if value not in set([0, 1, 2, 3]): + raise ValueError("must be one of enum values (0, 1, 2, 3)") + return value @field_validator('margin_mode') def margin_mode_validate_enum(cls, value): @@ -115,6 +128,9 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "asset_id": obj.get("asset_id"), "symbol": obj.get("symbol"), "l1_decimals": obj.get("l1_decimals"), + "evm_decimals": obj.get("evm_decimals"), + "canonical_domain": obj.get("canonical_domain"), + "evm_token": obj.get("evm_token"), "decimals": obj.get("decimals"), "min_transfer_amount": obj.get("min_transfer_amount"), "min_withdrawal_amount": obj.get("min_withdrawal_amount"), diff --git a/openapi.json b/openapi.json index 4c4656d0..5c119c34 100644 --- a/openapi.json +++ b/openapi.json @@ -5548,7 +5548,30 @@ "l1_decimals": { "type": "integer", "format": "uint8", - "example": 18 + "example": 18, + "description": "L1 token decimals when supplied by registration. Omitted for Core-native and EVM-native assets." + }, + "evm_decimals": { + "type": "integer", + "format": "uint8", + "example": 18, + "description": "Canonical EVM token decimals for EVM-native assets. Zero is a valid precision; omission means unavailable." + }, + "canonical_domain": { + "type": "integer", + "format": "uint8", + "enum": [ + 0, + 1, + 2, + 3 + ], + "description": "Asset origin: 0 legacy, 1 L1, 2 Core, 3 EVM. Omitted by older API versions." + }, + "evm_token": { + "type": "string", + "example": "0x0000000000000000000000000000000000000000", + "description": "Linked EVM token or wrapper address. The zero address means unlinked; older API versions omit this field." }, "decimals": { "type": "integer", @@ -5621,7 +5644,6 @@ "required": [ "asset_id", "symbol", - "l1_decimals", "decimals", "min_transfer_amount", "min_withdrawal_amount", diff --git a/test/test_asset_metadata.py b/test/test_asset_metadata.py new file mode 100644 index 00000000..fc6c35d7 --- /dev/null +++ b/test/test_asset_metadata.py @@ -0,0 +1,35 @@ +"""Decode asset metadata from legacy and native-domain API responses.""" + +import pytest + +from lighter.models.asset import Asset +from lighter.models.asset_details import AssetDetails + + +@pytest.mark.parametrize("metadata", [ + {"l1_decimals": 6}, + {"canonical_domain": 1, "l1_decimals": 18, "evm_token": "0x" + "11" * 20}, + {"canonical_domain": 2, "evm_token": "0x" + "00" * 20}, + {"canonical_domain": 3, "evm_decimals": 18, "evm_token": "0x" + "22" * 20}, + {"canonical_domain": 3, "evm_decimals": 0, "evm_token": "0x" + "00" * 20}, +]) +def test_asset_metadata_response(metadata): + payload = { + "asset_id": 7, "symbol": "TOKEN", "decimals": 8, + "min_transfer_amount": "0.01", "min_withdrawal_amount": "0.01", + "margin_mode": "disabled", "index_price": "2.500000000", + "l1_address": "0x" + "00" * 20, "price_decimals": 9, + "global_supply_cap": "0", "user_supply_cap": "0", "total_supplied": "0", + "liquidation_fee": "0", "liquidation_threshold": "0", "loan_to_value": "0", + "liquidation_factor": "0", "multiplier": "1", **metadata, + } + response = AssetDetails.from_dict({"code": 200, "asset_details": [payload]}) + asset = response.asset_details[0] + # Direct model validation must support the same field presence as HTTP decoding. + validated = Asset.model_validate(payload) + for field in ("l1_decimals", "evm_decimals", "canonical_domain", "evm_token"): + assert getattr(asset, field) == metadata.get(field) + assert getattr(validated, field) == metadata.get(field) + assert (field in asset.to_dict()) == (field in metadata) + assert asset.to_dict() == payload + assert asset.additional_properties == {}