2026-04-09 23:27:30 -04:00
|
|
|
from random import randint
|
|
|
|
|
from typing import Annotated, Any, List, Self, Union
|
2025-10-30 11:48:34 +01:00
|
|
|
|
2026-02-13 01:03:47 +04:00
|
|
|
from pydantic import BeforeValidator, Field
|
2025-10-30 11:48:34 +01:00
|
|
|
|
|
|
|
|
from core.models import NbeSerializer
|
2026-04-09 23:27:30 -04:00
|
|
|
from node.api.serializers.fields import BytesFromHex, BytesFromIntArray
|
|
|
|
|
from node.api.serializers.note import NoteSerializer
|
|
|
|
|
from utils.protocols import FromRandom
|
2025-10-30 11:48:34 +01:00
|
|
|
from utils.random import random_bytes
|
|
|
|
|
|
2026-04-09 23:27:30 -04:00
|
|
|
# Mantle op opcodes (new node release).
|
|
|
|
|
OPCODE_LEDGER = 0
|
|
|
|
|
OPCODE_CHANNEL_INSCRIBE = 17
|
2025-10-30 11:48:34 +01:00
|
|
|
|
|
|
|
|
|
2026-04-09 23:27:30 -04:00
|
|
|
class LedgerOpSerializer(NbeSerializer, FromRandom):
|
|
|
|
|
"""Mantle ledger op (opcode 0): consumes input notes and produces outputs."""
|
2025-10-30 11:48:34 +01:00
|
|
|
|
2026-04-09 23:27:30 -04:00
|
|
|
inputs: List[BytesFromHex] = Field(description="Input note IDs (Fr).")
|
|
|
|
|
outputs: List[NoteSerializer]
|
2025-10-30 11:48:34 +01:00
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_random(cls) -> Self:
|
2026-04-09 23:27:30 -04:00
|
|
|
n_inputs = 0 if randint(0, 1) <= 0.5 else randint(1, 5)
|
|
|
|
|
n_outputs = 0 if randint(0, 1) <= 0.5 else randint(1, 5)
|
2025-10-30 11:48:34 +01:00
|
|
|
return cls.model_validate(
|
|
|
|
|
{
|
2026-04-09 23:27:30 -04:00
|
|
|
"inputs": [random_bytes(32).hex() for _ in range(n_inputs)],
|
|
|
|
|
"outputs": [NoteSerializer.from_random() for _ in range(n_outputs)],
|
2025-10-30 11:48:34 +01:00
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-04-09 23:27:30 -04:00
|
|
|
class ChannelInscribeOpSerializer(NbeSerializer, FromRandom):
|
|
|
|
|
"""Channel inscribe op (opcode 17): writes an inscription to a channel."""
|
2025-10-30 11:48:34 +01:00
|
|
|
|
2026-04-09 23:27:30 -04:00
|
|
|
channel_id: BytesFromHex = Field(description="Channel ID in hex format.")
|
|
|
|
|
inscription: BytesFromIntArray = Field(description="Inscription bytes (int array).")
|
|
|
|
|
parent: BytesFromHex = Field(description="Parent inscription hash in hex format.")
|
|
|
|
|
signer: BytesFromHex = Field(description="Signer public key in hex format.")
|
2025-10-30 11:48:34 +01:00
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_random(cls) -> Self:
|
|
|
|
|
return cls.model_validate(
|
|
|
|
|
{
|
2026-04-09 23:27:30 -04:00
|
|
|
"channel_id": random_bytes(32).hex(),
|
|
|
|
|
"inscription": list(random_bytes(32)),
|
2026-02-13 01:03:47 +04:00
|
|
|
"parent": random_bytes(32).hex(),
|
2025-10-30 11:48:34 +01:00
|
|
|
"signer": random_bytes(32).hex(),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-04-09 23:27:30 -04:00
|
|
|
OPCODE_TO_SERIALIZER: dict[int, type] = {
|
|
|
|
|
OPCODE_LEDGER: LedgerOpSerializer,
|
|
|
|
|
OPCODE_CHANNEL_INSCRIBE: ChannelInscribeOpSerializer,
|
2026-02-13 01:03:47 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-04-09 23:27:30 -04:00
|
|
|
def _parse_mantle_op(data: Any) -> Union[LedgerOpSerializer, ChannelInscribeOpSerializer]:
|
|
|
|
|
if isinstance(data, (LedgerOpSerializer, ChannelInscribeOpSerializer)):
|
2026-02-13 01:03:47 +04:00
|
|
|
return data
|
|
|
|
|
if isinstance(data, dict) and "opcode" in data:
|
|
|
|
|
opcode = data["opcode"]
|
|
|
|
|
serializer_class = OPCODE_TO_SERIALIZER.get(opcode)
|
|
|
|
|
if serializer_class is None:
|
2026-04-09 23:27:30 -04:00
|
|
|
raise ValueError(
|
|
|
|
|
f"Unsupported mantle op opcode {opcode}; known opcodes: {sorted(OPCODE_TO_SERIALIZER)}."
|
|
|
|
|
)
|
|
|
|
|
return serializer_class.model_validate(data["payload"])
|
|
|
|
|
raise ValueError(f"Cannot parse mantle op from {type(data).__name__}.")
|
2026-02-13 01:03:47 +04:00
|
|
|
|
|
|
|
|
|
2026-04-09 23:27:30 -04:00
|
|
|
MantleOpSerializerVariants = Union[LedgerOpSerializer, ChannelInscribeOpSerializer]
|
|
|
|
|
MantleOpSerializerField = Annotated[MantleOpSerializerVariants, BeforeValidator(_parse_mantle_op)]
|