mirror of
https://github.com/logos-blockchain/logos-blockchain-block-explorer-template.git
synced 2026-05-18 07:19:27 +00:00
update tx schema to v0.1.1
This commit is contained in:
parent
db76ad97f8
commit
4fa63da82a
@ -3,7 +3,6 @@ from typing import List, Self
|
||||
from core.models import NbeSchema
|
||||
from core.types import HexBytes
|
||||
from models.aliases import Gas
|
||||
from models.transactions.notes import Note
|
||||
from models.transactions.operations.operation import Operation
|
||||
from models.transactions.transaction import Transaction
|
||||
|
||||
@ -13,9 +12,6 @@ class TransactionRead(NbeSchema):
|
||||
block_hash: HexBytes
|
||||
hash: HexBytes
|
||||
operations: List[Operation]
|
||||
inputs: List[HexBytes]
|
||||
outputs: List[Note]
|
||||
proof: HexBytes
|
||||
execution_gas_price: Gas
|
||||
storage_gas_price: Gas
|
||||
|
||||
@ -26,9 +22,6 @@ class TransactionRead(NbeSchema):
|
||||
block_hash=transaction.block.hash,
|
||||
hash=transaction.hash,
|
||||
operations=transaction.operations,
|
||||
inputs=transaction.inputs,
|
||||
outputs=transaction.outputs,
|
||||
proof=transaction.proof,
|
||||
execution_gas_price=transaction.execution_gas_price,
|
||||
storage_gas_price=transaction.storage_gas_price,
|
||||
)
|
||||
|
||||
@ -3,9 +3,11 @@ from typing import List, Literal, Optional
|
||||
|
||||
from core.models import NbeSchema
|
||||
from core.types import HexBytes
|
||||
from models.transactions.notes import Note
|
||||
|
||||
|
||||
class ContentType(Enum):
|
||||
LEDGER_TRANSFER = "LedgerTransfer"
|
||||
CHANNEL_INSCRIBE = "ChannelInscribe"
|
||||
CHANNEL_BLOB = "ChannelBlob"
|
||||
CHANNEL_SET_KEYS = "ChannelSetKeys"
|
||||
@ -19,6 +21,12 @@ class NbeContent(NbeSchema):
|
||||
type: str
|
||||
|
||||
|
||||
class LedgerTransfer(NbeContent):
|
||||
type: Literal["LedgerTransfer"] = "LedgerTransfer"
|
||||
inputs: List[HexBytes]
|
||||
outputs: List[Note]
|
||||
|
||||
|
||||
class ChannelInscribe(NbeContent):
|
||||
type: Literal["ChannelInscribe"] = "ChannelInscribe"
|
||||
channel_id: HexBytes
|
||||
@ -77,4 +85,6 @@ class LeaderClaim(NbeContent):
|
||||
mantle_tx_hash: HexBytes
|
||||
|
||||
|
||||
OperationContent = ChannelInscribe | ChannelBlob | ChannelSetKeys | SDPDeclare | SDPWithdraw | SDPActive | LeaderClaim
|
||||
OperationContent = (
|
||||
LedgerTransfer | ChannelInscribe | ChannelBlob | ChannelSetKeys | SDPDeclare | SDPWithdraw | SDPActive | LeaderClaim
|
||||
)
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from sqlalchemy import JSON, Column
|
||||
from sqlalchemy import Column
|
||||
from sqlmodel import Field, Relationship
|
||||
|
||||
from core.models import TimestampedModel
|
||||
from core.sqlmodel import PydanticJsonColumn
|
||||
from core.types import HexBytes
|
||||
from models.aliases import Fr, Gas
|
||||
from models.aliases import Gas
|
||||
from models.block import Block
|
||||
from models.transactions.notes import Note
|
||||
from models.transactions.operations.operation import Operation
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -25,11 +24,6 @@ class Transaction(TimestampedModel, table=True):
|
||||
operations: List[Operation] = Field(
|
||||
default_factory=list, sa_column=Column(PydanticJsonColumn(Operation, many=True), nullable=False)
|
||||
)
|
||||
inputs: List[Fr] = Field(default_factory=list, sa_column=Column(PydanticJsonColumn(Fr, many=True), nullable=False))
|
||||
outputs: List[Note] = Field(
|
||||
default_factory=list, sa_column=Column(PydanticJsonColumn(Note, many=True), nullable=False)
|
||||
)
|
||||
proof: HexBytes = Field(min_length=128, max_length=128, nullable=False)
|
||||
execution_gas_price: Gas
|
||||
storage_gas_price: Gas
|
||||
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
from random import randint
|
||||
from typing import List, Self
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from core.models import NbeSerializer
|
||||
from node.api.serializers.fields import BytesFromHex
|
||||
from node.api.serializers.note import NoteSerializer
|
||||
from utils.protocols import FromRandom
|
||||
from utils.random import random_bytes
|
||||
|
||||
|
||||
class LedgerTransactionSerializer(NbeSerializer, FromRandom):
|
||||
inputs: List[BytesFromHex] = Field(description="Fr integer.")
|
||||
outputs: List[NoteSerializer]
|
||||
|
||||
@classmethod
|
||||
def from_random(cls) -> Self:
|
||||
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)
|
||||
|
||||
return cls.model_validate(
|
||||
{
|
||||
"inputs": [random_bytes(32).hex() for _ in range(n_inputs)],
|
||||
"outputs": [NoteSerializer.from_random() for _ in range(n_outputs)],
|
||||
}
|
||||
)
|
||||
@ -1,48 +1,45 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum
|
||||
from random import choice, randint
|
||||
from typing import Annotated, Any, List, Optional, Self, Union
|
||||
from random import randint
|
||||
from typing import Annotated, Any, List, Self, Union
|
||||
|
||||
from pydantic import BeforeValidator, Field
|
||||
|
||||
from core.models import NbeSerializer
|
||||
from models.transactions.operations.contents import (
|
||||
ChannelBlob,
|
||||
ChannelInscribe,
|
||||
ChannelSetKeys,
|
||||
LeaderClaim,
|
||||
NbeContent,
|
||||
SDPActive,
|
||||
SDPDeclare,
|
||||
SDPWithdraw,
|
||||
)
|
||||
from node.api.serializers.fields import BytesFromHex, BytesFromInt, BytesFromIntArray
|
||||
from utils.protocols import EnforceSubclassFromRandom
|
||||
from node.api.serializers.fields import BytesFromHex, BytesFromIntArray
|
||||
from node.api.serializers.note import NoteSerializer
|
||||
from utils.protocols import FromRandom
|
||||
from utils.random import random_bytes
|
||||
|
||||
|
||||
class OperationContentSerializer(NbeSerializer, EnforceSubclassFromRandom, ABC):
|
||||
@abstractmethod
|
||||
def into_operation_content(self) -> NbeContent:
|
||||
raise NotImplementedError
|
||||
# Mantle op opcodes (new node release).
|
||||
OPCODE_LEDGER = 0
|
||||
OPCODE_CHANNEL_INSCRIBE = 17
|
||||
|
||||
|
||||
class ChannelInscribeSerializer(OperationContentSerializer):
|
||||
channel_id: BytesFromHex = Field(description="Channel ID in hex format.")
|
||||
inscription: BytesFromIntArray = Field(description="Bytes as an integer array.")
|
||||
parent: BytesFromHex = Field(description="Parent hash in hex format.")
|
||||
signer: BytesFromHex = Field(description="Public Key in hex format.")
|
||||
class LedgerOpSerializer(NbeSerializer, FromRandom):
|
||||
"""Mantle ledger op (opcode 0): consumes input notes and produces outputs."""
|
||||
|
||||
def into_operation_content(self) -> ChannelInscribe:
|
||||
return ChannelInscribe.model_validate(
|
||||
inputs: List[BytesFromHex] = Field(description="Input note IDs (Fr).")
|
||||
outputs: List[NoteSerializer]
|
||||
|
||||
@classmethod
|
||||
def from_random(cls) -> Self:
|
||||
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)
|
||||
return cls.model_validate(
|
||||
{
|
||||
"channel_id": self.channel_id,
|
||||
"inscription": self.inscription,
|
||||
"parent": self.parent,
|
||||
"signer": self.signer,
|
||||
"inputs": [random_bytes(32).hex() for _ in range(n_inputs)],
|
||||
"outputs": [NoteSerializer.from_random() for _ in range(n_outputs)],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ChannelInscribeOpSerializer(NbeSerializer, FromRandom):
|
||||
"""Channel inscribe op (opcode 17): writes an inscription to a channel."""
|
||||
|
||||
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.")
|
||||
|
||||
@classmethod
|
||||
def from_random(cls) -> Self:
|
||||
return cls.model_validate(
|
||||
@ -55,206 +52,25 @@ class ChannelInscribeSerializer(OperationContentSerializer):
|
||||
)
|
||||
|
||||
|
||||
class ChannelBlobSerializer(OperationContentSerializer):
|
||||
channel: BytesFromHex = Field(description="Channel ID in hex format.")
|
||||
blob: BytesFromIntArray = Field(description="Bytes as an integer array.")
|
||||
blob_size: int
|
||||
da_storage_gas_price: int
|
||||
parent: BytesFromHex = Field(description="Parent hash in hex format.")
|
||||
signer: BytesFromHex = Field(description="Public Key in hex format.")
|
||||
|
||||
def into_operation_content(self) -> ChannelBlob:
|
||||
return ChannelBlob.model_validate(
|
||||
{
|
||||
"channel": self.channel,
|
||||
"blob": self.blob,
|
||||
"blob_size": self.blob_size,
|
||||
"da_storage_gas_price": self.da_storage_gas_price,
|
||||
"parent": self.parent,
|
||||
"signer": self.signer,
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_random(cls) -> Self:
|
||||
return cls.model_validate(
|
||||
{
|
||||
"channel": random_bytes(32).hex(),
|
||||
"blob": list(random_bytes(32)),
|
||||
"blob_size": randint(1, 1_024),
|
||||
"da_storage_gas_price": randint(1, 10_000),
|
||||
"parent": random_bytes(32).hex(),
|
||||
"signer": random_bytes(32).hex(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ChannelSetKeysSerializer(OperationContentSerializer):
|
||||
channel: BytesFromHex = Field(description="Channel ID in hex format.")
|
||||
keys: List[BytesFromHex] = Field(description="List of Public Keys in hex format.")
|
||||
|
||||
def into_operation_content(self) -> ChannelSetKeys:
|
||||
return ChannelSetKeys.model_validate(
|
||||
{
|
||||
"channel": self.channel,
|
||||
"keys": self.keys,
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_random(cls) -> Self:
|
||||
n = 1 if randint(0, 1) <= 0.5 else randint(1, 5)
|
||||
return cls.model_validate(
|
||||
{
|
||||
"channel": random_bytes(32).hex(),
|
||||
"keys": [random_bytes(32).hex() for _ in range(n)],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class SDPDeclareServiceType(Enum):
|
||||
BN = "BN"
|
||||
DA = "DA"
|
||||
|
||||
|
||||
class SDPDeclareSerializer(OperationContentSerializer):
|
||||
service_type: SDPDeclareServiceType
|
||||
locators: List[BytesFromHex]
|
||||
provider_id: BytesFromHex = Field(description="Provider ID in hex format.")
|
||||
zk_id: BytesFromHex = Field(description="Fr integer.")
|
||||
locked_note_id: BytesFromHex = Field(description="Fr integer.")
|
||||
|
||||
def into_operation_content(self) -> SDPDeclare:
|
||||
return SDPDeclare.model_validate(
|
||||
{
|
||||
"service_type": self.service_type.value,
|
||||
"locators": self.locators,
|
||||
"provider_id": self.provider_id,
|
||||
"zk_id": self.zk_id,
|
||||
"locked_note_id": self.locked_note_id,
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_random(cls) -> Self:
|
||||
n = 1 if randint(0, 1) <= 0.5 else randint(1, 5)
|
||||
return cls.model_validate(
|
||||
{
|
||||
"service_type": choice(list(SDPDeclareServiceType)).value,
|
||||
"locators": [random_bytes(32).hex() for _ in range(n)],
|
||||
"provider_id": random_bytes(32).hex(),
|
||||
"zk_id": random_bytes(32).hex(),
|
||||
"locked_note_id": random_bytes(32).hex(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class SDPWithdrawSerializer(OperationContentSerializer):
|
||||
declaration_id: BytesFromHex = Field(description="Declaration ID in hex format.")
|
||||
nonce: BytesFromInt
|
||||
|
||||
def into_operation_content(self) -> SDPWithdraw:
|
||||
return SDPWithdraw.model_validate(
|
||||
{
|
||||
"declaration_id": self.declaration_id,
|
||||
"nonce": self.nonce,
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_random(cls) -> Self:
|
||||
return cls.model_validate(
|
||||
{
|
||||
"declaration_id": random_bytes(32).hex(),
|
||||
"nonce": int.from_bytes(random_bytes(8)),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class SDPActiveSerializer(OperationContentSerializer):
|
||||
declaration_id: BytesFromHex = Field(description="Declaration ID in hex format.")
|
||||
nonce: BytesFromInt
|
||||
metadata: Optional[BytesFromIntArray] = Field(description="Bytes as an integer array.")
|
||||
|
||||
def into_operation_content(self) -> SDPActive:
|
||||
return SDPActive.model_validate(
|
||||
{
|
||||
"declaration_id": self.declaration_id,
|
||||
"nonce": self.nonce,
|
||||
"metadata": self.metadata,
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_random(cls) -> Self:
|
||||
return cls.model_validate(
|
||||
{
|
||||
"declaration_id": random_bytes(32).hex(),
|
||||
"nonce": int.from_bytes(random_bytes(8)),
|
||||
"metadata": None if randint(0, 1) <= 0.5 else list(random_bytes(32)),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class LeaderClaimSerializer(OperationContentSerializer):
|
||||
rewards_root: BytesFromInt = Field(description="Fr integer.")
|
||||
voucher_nullifier: BytesFromInt = Field(description="Fr integer.")
|
||||
mantle_tx_hash: BytesFromInt = Field(description="Fr integer.")
|
||||
|
||||
def into_operation_content(self) -> LeaderClaim:
|
||||
return LeaderClaim.model_validate(
|
||||
{
|
||||
"rewards_root": self.rewards_root,
|
||||
"voucher_nullifier": self.voucher_nullifier,
|
||||
"mantle_tx_hash": self.mantle_tx_hash,
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_random(cls) -> Self:
|
||||
return cls.model_validate(
|
||||
{
|
||||
"rewards_root": int.from_bytes(random_bytes(8)),
|
||||
"voucher_nullifier": int.from_bytes(random_bytes(8)),
|
||||
"mantle_tx_hash": int.from_bytes(random_bytes(8)),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
OPCODE_TO_SERIALIZER: dict[int, type[OperationContentSerializer]] = {
|
||||
0: ChannelInscribeSerializer,
|
||||
1: ChannelBlobSerializer,
|
||||
2: ChannelSetKeysSerializer,
|
||||
3: SDPDeclareSerializer,
|
||||
4: SDPWithdrawSerializer,
|
||||
5: SDPActiveSerializer,
|
||||
6: LeaderClaimSerializer,
|
||||
OPCODE_TO_SERIALIZER: dict[int, type] = {
|
||||
OPCODE_LEDGER: LedgerOpSerializer,
|
||||
OPCODE_CHANNEL_INSCRIBE: ChannelInscribeOpSerializer,
|
||||
}
|
||||
|
||||
|
||||
def _parse_operation(data: Any) -> OperationContentSerializer:
|
||||
if isinstance(data, OperationContentSerializer):
|
||||
def _parse_mantle_op(data: Any) -> Union[LedgerOpSerializer, ChannelInscribeOpSerializer]:
|
||||
if isinstance(data, (LedgerOpSerializer, ChannelInscribeOpSerializer)):
|
||||
return data
|
||||
if isinstance(data, dict) and "opcode" in data:
|
||||
opcode = data["opcode"]
|
||||
payload = data["payload"]
|
||||
serializer_class = OPCODE_TO_SERIALIZER.get(opcode)
|
||||
if serializer_class is None:
|
||||
raise ValueError(f"Unknown operation opcode: {opcode}")
|
||||
return serializer_class.model_validate(payload)
|
||||
return data
|
||||
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__}.")
|
||||
|
||||
|
||||
type OperationContentSerializerVariants = Union[
|
||||
ChannelInscribeSerializer,
|
||||
ChannelBlobSerializer,
|
||||
ChannelSetKeysSerializer,
|
||||
SDPDeclareSerializer,
|
||||
SDPWithdrawSerializer,
|
||||
SDPActiveSerializer,
|
||||
LeaderClaimSerializer,
|
||||
]
|
||||
OperationContentSerializerField = Annotated[
|
||||
OperationContentSerializerVariants,
|
||||
BeforeValidator(_parse_operation),
|
||||
]
|
||||
MantleOpSerializerVariants = Union[LedgerOpSerializer, ChannelInscribeOpSerializer]
|
||||
MantleOpSerializerField = Annotated[MantleOpSerializerVariants, BeforeValidator(_parse_mantle_op)]
|
||||
|
||||
@ -36,19 +36,32 @@ class Ed25519SignatureSerializer(OperationProofSerializer, RootModel[bytes]):
|
||||
return cls.model_validate(list(random_bytes(64)))
|
||||
|
||||
|
||||
class ZkSignatureSerializer(OperationProofSerializer, RootModel[bytes]):
|
||||
root: BytesFromIntArray
|
||||
class ZkSignatureSerializer(OperationProofSerializer, NbeSerializer):
|
||||
"""Groth16 ZK proof: pi_a (32B) + pi_b (64B) + pi_c (32B) = 128 bytes total."""
|
||||
|
||||
pi_a: BytesFromIntArray
|
||||
pi_b: BytesFromIntArray
|
||||
pi_c: BytesFromIntArray
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
return self.pi_a + self.pi_b + self.pi_c
|
||||
|
||||
def into_operation_proof(self) -> NbeSignature:
|
||||
return ZkSignature.model_validate(
|
||||
{
|
||||
"signature": self.root,
|
||||
"signature": self.to_bytes(),
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_random(cls, *args, **kwargs) -> Self:
|
||||
return cls.model_validate(list(random_bytes(32)))
|
||||
return cls.model_validate(
|
||||
{
|
||||
"pi_a": list(random_bytes(32)),
|
||||
"pi_b": list(random_bytes(64)),
|
||||
"pi_c": list(random_bytes(32)),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ZkAndEd25519SignaturesSerializer(OperationProofSerializer, NbeSerializer):
|
||||
|
||||
@ -6,41 +6,25 @@ from pydantic import Field
|
||||
|
||||
from core.models import NbeSerializer
|
||||
from models.transactions.transaction import Transaction
|
||||
from node.api.serializers.fields import BytesFromIntArray
|
||||
from node.api.serializers.operation import (
|
||||
ChannelInscribeOpSerializer,
|
||||
LedgerOpSerializer,
|
||||
)
|
||||
from node.api.serializers.proof import (
|
||||
OperationProofSerializer,
|
||||
Ed25519SignatureSerializer,
|
||||
OperationProofSerializerField,
|
||||
ZkSignatureSerializer,
|
||||
)
|
||||
from node.api.serializers.transaction import TransactionSerializer
|
||||
from utils.protocols import FromRandom
|
||||
from utils.random import random_bytes
|
||||
|
||||
|
||||
class Groth16ProofSerializer(NbeSerializer, FromRandom):
|
||||
pi_a: BytesFromIntArray
|
||||
pi_b: BytesFromIntArray
|
||||
pi_c: BytesFromIntArray
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
return self.pi_a + self.pi_b + self.pi_c
|
||||
|
||||
@classmethod
|
||||
def from_random(cls) -> Self:
|
||||
return cls.model_validate(
|
||||
{
|
||||
"pi_a": list(random_bytes(32)),
|
||||
"pi_b": list(random_bytes(64)),
|
||||
"pi_c": list(random_bytes(32)),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class SignedTransactionSerializer(NbeSerializer, FromRandom):
|
||||
transaction: TransactionSerializer = Field(alias="mantle_tx", description="Transaction.")
|
||||
operations_proofs: List[OperationProofSerializerField] = Field(
|
||||
alias="ops_proofs", description="List of OperationProof. Order should match `Self::transaction::operations`."
|
||||
alias="ops_proofs",
|
||||
description="List of OperationProof. Order should match `Self::transaction::ops`.",
|
||||
)
|
||||
ledger_transaction_proof: Groth16ProofSerializer = Field(alias="ledger_tx_proof", description="Groth16 proof.")
|
||||
|
||||
def _compute_hash(self) -> bytes:
|
||||
data = self.transaction.model_dump(mode="json")
|
||||
@ -48,30 +32,60 @@ class SignedTransactionSerializer(NbeSerializer, FromRandom):
|
||||
return hashlib.sha256(canonical.encode()).digest()
|
||||
|
||||
def into_transaction(self) -> Transaction:
|
||||
operations_contents = self.transaction.operations_contents
|
||||
if len(operations_contents) != len(self.operations_proofs):
|
||||
ops = self.transaction.ops
|
||||
if len(ops) != len(self.operations_proofs):
|
||||
raise ValueError(
|
||||
f"Number of operations ({len(operations_contents)}) does not match number of operation proofs ({len(self.operations_proofs)})."
|
||||
f"Number of ops ({len(ops)}) does not match number of op proofs "
|
||||
f"({len(self.operations_proofs)})."
|
||||
)
|
||||
|
||||
operations = [
|
||||
{
|
||||
"content": content.into_operation_content(),
|
||||
"proof": proof.into_operation_proof(),
|
||||
}
|
||||
for content, proof in zip(operations_contents, self.operations_proofs)
|
||||
]
|
||||
|
||||
ledger_transaction = self.transaction.ledger_transaction
|
||||
outputs = [output.into_note() for output in ledger_transaction.outputs]
|
||||
operations: List[dict] = []
|
||||
for op, proof in zip(ops, self.operations_proofs):
|
||||
if isinstance(op, LedgerOpSerializer):
|
||||
if not isinstance(proof, ZkSignatureSerializer):
|
||||
raise ValueError(
|
||||
f"Expected a ZkSig (Groth16) proof for the ledger op, got {type(proof).__name__}."
|
||||
)
|
||||
operations.append(
|
||||
{
|
||||
"content": {
|
||||
"type": "LedgerTransfer",
|
||||
"inputs": list(op.inputs),
|
||||
"outputs": [o.into_note() for o in op.outputs],
|
||||
},
|
||||
"proof": {
|
||||
"type": "Zk",
|
||||
"signature": proof.to_bytes(),
|
||||
},
|
||||
}
|
||||
)
|
||||
elif isinstance(op, ChannelInscribeOpSerializer):
|
||||
if not isinstance(proof, Ed25519SignatureSerializer):
|
||||
raise ValueError(
|
||||
f"Expected an Ed25519Sig proof for the channel inscribe op, got {type(proof).__name__}."
|
||||
)
|
||||
operations.append(
|
||||
{
|
||||
"content": {
|
||||
"type": "ChannelInscribe",
|
||||
"channel_id": op.channel_id,
|
||||
"inscription": op.inscription,
|
||||
"parent": op.parent,
|
||||
"signer": op.signer,
|
||||
},
|
||||
"proof": {
|
||||
"type": "Ed25519",
|
||||
"signature": proof.root,
|
||||
},
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unsupported mantle op type: {type(op).__name__}")
|
||||
|
||||
return Transaction.model_validate(
|
||||
{
|
||||
"hash": self._compute_hash(),
|
||||
"operations": operations,
|
||||
"inputs": ledger_transaction.inputs,
|
||||
"outputs": outputs,
|
||||
"proof": self.ledger_transaction_proof.to_bytes(),
|
||||
"execution_gas_price": self.transaction.execution_gas_price,
|
||||
"storage_gas_price": self.transaction.storage_gas_price,
|
||||
}
|
||||
@ -80,12 +94,10 @@ class SignedTransactionSerializer(NbeSerializer, FromRandom):
|
||||
@classmethod
|
||||
def from_random(cls) -> Self:
|
||||
transaction = TransactionSerializer.from_random()
|
||||
n = len(transaction.operations_contents)
|
||||
operations_proofs = [OperationProofSerializer.from_random() for _ in range(n)]
|
||||
operations_proofs = [ZkSignatureSerializer.from_random() for _ in range(len(transaction.ops))]
|
||||
return cls.model_validate(
|
||||
{
|
||||
"mantle_tx": transaction,
|
||||
"ops_proofs": operations_proofs,
|
||||
"ledger_tx_proof": Groth16ProofSerializer.from_random(),
|
||||
}
|
||||
)
|
||||
|
||||
@ -4,29 +4,22 @@ from typing import List, Self
|
||||
from pydantic import Field
|
||||
|
||||
from core.models import NbeSerializer
|
||||
from node.api.serializers.ledger_transaction import LedgerTransactionSerializer
|
||||
from node.api.serializers.operation import (
|
||||
OperationContentSerializer,
|
||||
OperationContentSerializerField,
|
||||
)
|
||||
from node.api.serializers.operation import LedgerOpSerializer, MantleOpSerializerField
|
||||
from utils.protocols import FromRandom
|
||||
from utils.random import random_bytes
|
||||
|
||||
|
||||
class TransactionSerializer(NbeSerializer, FromRandom):
|
||||
operations_contents: List[OperationContentSerializerField] = Field(alias="ops")
|
||||
ledger_transaction: LedgerTransactionSerializer = Field(alias="ledger_tx")
|
||||
ops: List[MantleOpSerializerField]
|
||||
execution_gas_price: int = Field(description="Integer in u64 format.")
|
||||
storage_gas_price: int = Field(description="Integer in u64 format.")
|
||||
|
||||
@classmethod
|
||||
def from_random(cls) -> Self:
|
||||
n = 0 if randint(0, 1) <= 0.5 else randint(1, 5)
|
||||
operations_contents = [OperationContentSerializer.from_random() for _ in range(n)]
|
||||
n = 1 if randint(0, 1) <= 0.5 else randint(1, 3)
|
||||
ops = [LedgerOpSerializer.from_random() for _ in range(n)]
|
||||
return cls.model_validate(
|
||||
{
|
||||
"ops": operations_contents,
|
||||
"ledger_tx": LedgerTransactionSerializer.from_random(),
|
||||
"ops": ops,
|
||||
"execution_gas_price": randint(1, 10_000),
|
||||
"storage_gas_price": randint(1, 10_000),
|
||||
}
|
||||
|
||||
@ -83,9 +83,21 @@ function formatOperationsPreview(ops) {
|
||||
}
|
||||
|
||||
// ---------- normalize API → view model ----------
|
||||
// Outputs come from the LedgerTransfer op(s) since the top-level inputs/outputs
|
||||
// columns were removed (the new mantle schema represents transfers as ops).
|
||||
function collectTransferOutputs(ops) {
|
||||
const outputs = [];
|
||||
for (const op of ops) {
|
||||
const content = op?.content ?? op;
|
||||
if (content?.type !== 'LedgerTransfer') continue;
|
||||
if (Array.isArray(content.outputs)) outputs.push(...content.outputs);
|
||||
}
|
||||
return outputs;
|
||||
}
|
||||
|
||||
function normalize(raw) {
|
||||
const ops = Array.isArray(raw?.operations) ? raw.operations : Array.isArray(raw?.ops) ? raw.ops : [];
|
||||
const outputs = Array.isArray(raw?.outputs) ? raw.outputs : [];
|
||||
const outputs = collectTransferOutputs(ops);
|
||||
const totalOutputValue = outputs.reduce((sum, note) => sum + toNumber(note?.value), 0);
|
||||
|
||||
return {
|
||||
|
||||
@ -40,7 +40,14 @@ function opsToPills(ops, limit = OPERATIONS_PREVIEW_LIMIT) {
|
||||
}
|
||||
|
||||
function computeOutputsSummaryFromTx(tx) {
|
||||
const outputs = Array.isArray(tx?.outputs) ? tx.outputs : [];
|
||||
// Outputs now live inside LedgerTransfer ops; aggregate across them.
|
||||
const ops = Array.isArray(tx?.operations) ? tx.operations : [];
|
||||
const outputs = [];
|
||||
for (const op of ops) {
|
||||
const content = op?.content ?? op;
|
||||
if (content?.type !== 'LedgerTransfer') continue;
|
||||
if (Array.isArray(content.outputs)) outputs.push(...content.outputs);
|
||||
}
|
||||
const count = outputs.length;
|
||||
const total = outputs.reduce((sum, o) => sum + Number(o?.value ?? 0), 0);
|
||||
return { count, total };
|
||||
|
||||
@ -94,35 +94,21 @@ function CopyPill({ text, label = 'Copy' }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ————— normalizer for new TransactionRead —————
|
||||
// { id, block_id, hash, operations:[Operation], inputs:[HexBytes], outputs:[Note{public_key:HexBytes,value:int}],
|
||||
// proof, execution_gas_price, storage_gas_price }
|
||||
// ————— normalizer for TransactionRead —————
|
||||
// { id, block_hash, hash, operations:[Operation], execution_gas_price, storage_gas_price }
|
||||
// Ledger transfers are now an Operation with content.type === 'LedgerTransfer'.
|
||||
function normalizeTransaction(raw) {
|
||||
const ops = Array.isArray(raw?.operations) ? raw.operations : Array.isArray(raw?.ops) ? raw.ops : [];
|
||||
|
||||
const inputs = Array.isArray(raw?.inputs) ? raw.inputs : [];
|
||||
const outputs = Array.isArray(raw?.outputs) ? raw.outputs : [];
|
||||
|
||||
const totalOutputValue = outputs.reduce((sum, note) => sum + toNumber(note?.value), 0);
|
||||
|
||||
return {
|
||||
id: raw?.id ?? '',
|
||||
blockHash: raw?.block_hash ?? null,
|
||||
hash: renderBytes(raw?.hash),
|
||||
proof: renderBytes(raw?.proof),
|
||||
operations: ops, // keep objects, we’ll label in UI
|
||||
operations: ops,
|
||||
executionGasPrice: isNumber(raw?.execution_gas_price)
|
||||
? raw.execution_gas_price
|
||||
: toNumber(raw?.execution_gas_price),
|
||||
storageGasPrice: isNumber(raw?.storage_gas_price) ? raw.storage_gas_price : toNumber(raw?.storage_gas_price),
|
||||
ledger: {
|
||||
inputs: inputs.map((v) => renderBytes(v)),
|
||||
outputs: outputs.map((n) => ({
|
||||
public_key: renderBytes(n?.public_key),
|
||||
value: toNumber(n?.value),
|
||||
})),
|
||||
totalOutputValue,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -170,20 +156,6 @@ function Summary({ tx }) {
|
||||
h(CopyPill, { text: tx.hash }),
|
||||
),
|
||||
|
||||
// Proof + copy (if present)
|
||||
tx.proof &&
|
||||
h(
|
||||
'div',
|
||||
null,
|
||||
h('b', null, 'Proof: '),
|
||||
h(
|
||||
'span',
|
||||
{ class: 'pill mono', title: tx.proof, style: 'max-width:100%; overflow-wrap:anywhere;' },
|
||||
String(tx.proof),
|
||||
),
|
||||
h(CopyPill, { text: tx.proof }),
|
||||
),
|
||||
|
||||
// Gas
|
||||
h(
|
||||
'div',
|
||||
@ -360,7 +332,55 @@ function InscriptionValue({ value }) {
|
||||
return h(FieldValue, { value });
|
||||
}
|
||||
|
||||
function LedgerTransferContent({ content }) {
|
||||
const inputs = Array.isArray(content?.inputs) ? content.inputs.map((v) => renderBytes(v)) : [];
|
||||
const rawOutputs = Array.isArray(content?.outputs) ? content.outputs : [];
|
||||
const outputs = rawOutputs.map((n) => ({
|
||||
public_key: renderBytes(n?.public_key),
|
||||
value: toNumber(n?.value),
|
||||
}));
|
||||
const totalOutputValue = outputs.reduce((sum, o) => sum + o.value, 0);
|
||||
|
||||
return h(
|
||||
'div',
|
||||
{ style: 'display:grid; gap:16px;' },
|
||||
|
||||
// Inputs
|
||||
h(
|
||||
'div',
|
||||
null,
|
||||
h(
|
||||
'div',
|
||||
{ style: 'display:flex; align-items:center; gap:8px;' },
|
||||
h('b', null, 'Inputs'),
|
||||
h('span', { class: 'pill' }, String(inputs.length)),
|
||||
),
|
||||
h(InputsTable, { inputs }),
|
||||
),
|
||||
|
||||
// Outputs
|
||||
h(
|
||||
'div',
|
||||
null,
|
||||
h(
|
||||
'div',
|
||||
{ style: 'display:flex; align-items:center; gap:8px;' },
|
||||
h('b', null, 'Outputs'),
|
||||
h('span', { class: 'pill' }, String(outputs.length)),
|
||||
h(
|
||||
'span',
|
||||
{ class: 'amount', style: 'margin-left:auto;' },
|
||||
`Total: ${toLocaleNum(totalOutputValue)}`,
|
||||
),
|
||||
),
|
||||
h(OutputsTable, { outputs }),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function OperationContent({ content }) {
|
||||
if (content?.type === 'LedgerTransfer') return h(LedgerTransferContent, { content });
|
||||
|
||||
// Get all fields except "type"
|
||||
const entries = Object.entries(content).filter(([k]) => k !== 'type');
|
||||
if (!entries.length) return h('div', { style: 'color:var(--muted)' }, 'No fields');
|
||||
@ -436,52 +456,6 @@ function Operations({ operations }) {
|
||||
);
|
||||
}
|
||||
|
||||
function Ledger({ ledger }) {
|
||||
const inputs = Array.isArray(ledger?.inputs) ? ledger.inputs : [];
|
||||
const outputs = Array.isArray(ledger?.outputs) ? ledger.outputs : [];
|
||||
const totalOutputValue = toNumber(ledger?.totalOutputValue);
|
||||
|
||||
return h(
|
||||
SectionCard,
|
||||
{ title: 'Ledger' },
|
||||
h(
|
||||
'div',
|
||||
{ style: 'display:grid; gap:16px;' },
|
||||
|
||||
// Inputs
|
||||
h(
|
||||
'div',
|
||||
null,
|
||||
h(
|
||||
'div',
|
||||
{ style: 'display:flex; alignItems:center; gap:8px;' },
|
||||
h('b', null, 'Inputs'),
|
||||
h('span', { class: 'pill' }, String(inputs.length)),
|
||||
),
|
||||
h(InputsTable, { inputs }),
|
||||
),
|
||||
|
||||
// Outputs
|
||||
h(
|
||||
'div',
|
||||
null,
|
||||
h(
|
||||
'div',
|
||||
{ style: 'display:flex; alignItems:center; gap:8px;' },
|
||||
h('b', null, 'Outputs'),
|
||||
h('span', { class: 'pill' }, String(outputs.length)),
|
||||
h(
|
||||
'span',
|
||||
{ class: 'amount', style: 'margin-left:auto;' },
|
||||
`Total: ${toLocaleNum(totalOutputValue)}`,
|
||||
),
|
||||
),
|
||||
h(OutputsTable, { outputs }),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ————— page —————
|
||||
export default function TransactionDetail({ parameters }) {
|
||||
const transactionHash = parameters?.[0];
|
||||
@ -571,7 +545,6 @@ export default function TransactionDetail({ parameters }) {
|
||||
null,
|
||||
h(Summary, { tx }),
|
||||
h(Operations, { operations: tx.operations }),
|
||||
h(Ledger, { ledger: tx.ledger }),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user