feat: Introduce 'traverse' library (#117)
This commit is contained in:
parent
fe80ff7800
commit
26da913ad9
20 changed files with 377 additions and 106 deletions
|
|
@ -6,6 +6,7 @@ members = [
|
|||
"pallas-miniprotocols",
|
||||
"pallas-crypto",
|
||||
"pallas-primitives",
|
||||
"pallas-traverse",
|
||||
"pallas",
|
||||
"examples/block-download",
|
||||
"examples/block-decode",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ As already explained, _Pallas_ aims at being an expanding set of components. The
|
|||
| Crates | Description |
|
||||
| --------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| [pallas-primitives](/pallas-primitives) | Ledger primitives and cbor codec for the different Cardano eras |
|
||||
| [pallas-traverse](/pallas-traverse) | Utilities to traverse over multi-era block data |
|
||||
| pallas-ticking | Time passage implementation for consensus algorithm |
|
||||
| pallas-applying | Logic for validating and applying new blocks and txs to the chain state |
|
||||
| pallas-forecasting | Ledger forecasting algorithm to be used by the consensus layer |
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use pallas::ledger::primitives::{alonzo, byron, probing, Era};
|
||||
use pallas::ledger::traverse::MultiEraBlock;
|
||||
|
||||
fn main() {
|
||||
let blocks = vec![
|
||||
|
|
@ -10,23 +10,14 @@ fn main() {
|
|||
];
|
||||
|
||||
for block_str in blocks.iter() {
|
||||
let bytes = hex::decode(block_str).expect("invalid hex");
|
||||
let cbor = hex::decode(block_str).expect("invalid hex");
|
||||
|
||||
match probing::probe_block_cbor_era(&bytes) {
|
||||
probing::Outcome::Matched(era) => match era {
|
||||
Era::Byron => {
|
||||
let (_, block): (u16, byron::MainBlock) =
|
||||
pallas::codec::minicbor::decode(&bytes).expect("invalid cbor");
|
||||
println!("{:?}", block)
|
||||
}
|
||||
// we use alonzo for everything post-shelly since it's backward compatible
|
||||
Era::Shelley | Era::Allegra | Era::Mary | Era::Alonzo => {
|
||||
let (_, block): (u16, alonzo::Block) =
|
||||
pallas::codec::minicbor::decode(&bytes).expect("invalid cbor");
|
||||
println!("{:?}", block)
|
||||
}
|
||||
},
|
||||
_ => println!("couldn't infer block era"),
|
||||
};
|
||||
let block = MultiEraBlock::decode(&cbor).expect("invalid cbor");
|
||||
|
||||
println!("{} {}", block.slot(), block.hash());
|
||||
|
||||
for tx in block.tx_iter() {
|
||||
println!("{:?}", tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ where
|
|||
}
|
||||
|
||||
/// A struct that maintains a reference to whether a cbor array was indef or not
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||
pub enum MaybeIndefArray<A> {
|
||||
Def(Vec<A>),
|
||||
Indef(Vec<A>),
|
||||
|
|
@ -186,7 +186,7 @@ where
|
|||
/// transform key-value structures into an orderer vec of `properties`, where
|
||||
/// each entry represents a a cbor-encodable variant of an attribute of the
|
||||
/// struct.
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct OrderPreservingProperties<P>(Vec<P>);
|
||||
|
||||
impl<P> Deref for OrderPreservingProperties<P> {
|
||||
|
|
@ -229,7 +229,7 @@ where
|
|||
}
|
||||
|
||||
/// Wraps a struct so that it is encoded/decoded as a cbor bytes
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CborWrap<T>(pub T);
|
||||
|
||||
impl<'b, C, T> minicbor::Decode<'b, C> for CborWrap<T>
|
||||
|
|
@ -312,7 +312,7 @@ where
|
|||
/// An empty map
|
||||
///
|
||||
/// don't ask me why, that's what the CDDL asks for.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EmptyMap;
|
||||
|
||||
impl<'b, C> minicbor::decode::Decode<'b, C> for EmptyMap {
|
||||
|
|
@ -518,7 +518,7 @@ impl From<&AnyUInt> for u64 {
|
|||
/// let confirm: (u16, u16) = minicbor::decode(keeper.raw_cbor()).unwrap();
|
||||
/// assert_eq!(confirm, (456u16, 789u16));
|
||||
/// ```
|
||||
#[derive(Debug, PartialEq, PartialOrd)]
|
||||
#[derive(Debug, PartialEq, PartialOrd, Clone)]
|
||||
pub struct KeepRaw<'b, T> {
|
||||
raw: &'b [u8],
|
||||
inner: T,
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ pub struct Header {
|
|||
pub body_signature: ByteVec,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct TransactionInput {
|
||||
#[n(0)]
|
||||
pub transaction_id: Hash<32>,
|
||||
|
|
@ -85,7 +85,7 @@ pub struct TransactionInput {
|
|||
|
||||
// $nonce /= [ 0 // 1, bytes .size 32 ]
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
#[cbor(index_only)]
|
||||
pub enum NonceVariant {
|
||||
#[n(0)]
|
||||
|
|
@ -95,7 +95,7 @@ pub enum NonceVariant {
|
|||
Nonce,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct Nonce {
|
||||
#[n(0)]
|
||||
pub variant: NonceVariant,
|
||||
|
|
@ -162,7 +162,7 @@ impl<C> minicbor::encode::Encode<C> for Value {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct TransactionOutput {
|
||||
#[n(0)]
|
||||
pub address: ByteVec,
|
||||
|
|
@ -187,7 +187,7 @@ pub type VrfKeyhash = Hash<32>;
|
|||
; otherwise the funds are given to the other accounting pot.
|
||||
*/
|
||||
|
||||
#[derive(Debug, PartialEq, PartialOrd)]
|
||||
#[derive(Debug, PartialEq, PartialOrd, Clone)]
|
||||
pub enum InstantaneousRewardSource {
|
||||
Reserves,
|
||||
Treasury,
|
||||
|
|
@ -225,7 +225,7 @@ impl<C> minicbor::encode::Encode<C> for InstantaneousRewardSource {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, PartialOrd)]
|
||||
#[derive(Debug, PartialEq, PartialOrd, Clone)]
|
||||
pub enum InstantaneousRewardTarget {
|
||||
StakeCredentials(KeyValuePairs<StakeCredential, i64>),
|
||||
OtherAccountingPot(Coin),
|
||||
|
|
@ -267,7 +267,7 @@ impl<C> minicbor::encode::Encode<C> for InstantaneousRewardTarget {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq, PartialOrd)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, PartialOrd, Clone)]
|
||||
#[cbor]
|
||||
pub struct MoveInstantaneousReward {
|
||||
#[n(0)]
|
||||
|
|
@ -284,7 +284,7 @@ pub type IPv4 = ByteVec;
|
|||
pub type IPv6 = ByteVec;
|
||||
pub type DnsName = String;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum Relay {
|
||||
SingleHostAddr(Option<Port>, Option<IPv4>, Option<IPv6>),
|
||||
SingleHostName(Option<Port>, DnsName),
|
||||
|
|
@ -351,7 +351,7 @@ impl<C> minicbor::encode::Encode<C> for Relay {
|
|||
|
||||
pub type PoolMetadataHash = Hash<32>;
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct PoolMetadata {
|
||||
#[n(0)]
|
||||
pub url: String,
|
||||
|
|
@ -363,7 +363,7 @@ pub struct PoolMetadata {
|
|||
pub type AddrKeyhash = Hash<28>;
|
||||
pub type Scripthash = Hash<28>;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct RationalNumber {
|
||||
pub numerator: i64,
|
||||
pub denominator: u64,
|
||||
|
|
@ -401,7 +401,7 @@ pub type UnitInterval = RationalNumber;
|
|||
|
||||
pub type PositiveInterval = RationalNumber;
|
||||
|
||||
#[derive(Debug, PartialEq, PartialOrd, Eq, Ord)]
|
||||
#[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Clone)]
|
||||
pub enum StakeCredential {
|
||||
AddrKeyhash(AddrKeyhash),
|
||||
Scripthash(Scripthash),
|
||||
|
|
@ -447,7 +447,7 @@ impl<C> minicbor::encode::Encode<C> for StakeCredential {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum Certificate {
|
||||
StakeRegistration(StakeCredential),
|
||||
StakeDeregistration(StakeCredential),
|
||||
|
|
@ -615,7 +615,7 @@ impl<C> minicbor::encode::Encode<C> for Certificate {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||
#[cbor(index_only)]
|
||||
pub enum NetworkId {
|
||||
#[n(0)]
|
||||
|
|
@ -624,7 +624,7 @@ pub enum NetworkId {
|
|||
Two,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
#[cbor(index_only)]
|
||||
pub enum Language {
|
||||
#[n(0)]
|
||||
|
|
@ -637,7 +637,7 @@ pub type CostMdls = KeyValuePairs<Language, CostModel>;
|
|||
|
||||
pub type ProtocolVersion = (u32, u32);
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
#[cbor(map)]
|
||||
pub struct ProtocolParamUpdate {
|
||||
#[n(0)]
|
||||
|
|
@ -690,7 +690,7 @@ pub struct ProtocolParamUpdate {
|
|||
pub max_collateral_inputs: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct Update {
|
||||
#[n(0)]
|
||||
pub proposed_protocol_parameter_updates: KeyValuePairs<Genesishash, ProtocolParamUpdate>,
|
||||
|
|
@ -699,7 +699,7 @@ pub struct Update {
|
|||
pub epoch: Epoch,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum TransactionBodyComponent {
|
||||
Inputs(MaybeIndefArray<TransactionInput>),
|
||||
Outputs(MaybeIndefArray<TransactionOutput>),
|
||||
|
|
@ -814,7 +814,7 @@ impl<C> minicbor::encode::Encode<C> for TransactionBodyComponent {
|
|||
|
||||
// Can't derive encode for TransactionBody because it seems to require a very
|
||||
// particular order for each key in the map
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct TransactionBody(Vec<TransactionBodyComponent>);
|
||||
|
||||
impl Deref for TransactionBody {
|
||||
|
|
@ -850,7 +850,7 @@ impl<C> minicbor::encode::Encode<C> for TransactionBody {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct VKeyWitness {
|
||||
#[n(0)]
|
||||
pub vkey: ByteVec,
|
||||
|
|
@ -859,7 +859,7 @@ pub struct VKeyWitness {
|
|||
pub signature: ByteVec,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum NativeScript {
|
||||
ScriptPubkey(AddrKeyhash),
|
||||
ScriptAll(MaybeIndefArray<NativeScript>),
|
||||
|
|
@ -931,7 +931,7 @@ impl<C> minicbor::encode::Encode<C> for NativeScript {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
#[cbor(transparent)]
|
||||
pub struct PlutusScript(#[n(0)] pub ByteVec);
|
||||
|
||||
|
|
@ -947,7 +947,7 @@ big_uint = #6.2(bounded_bytes) ; New
|
|||
big_nint = #6.3(bounded_bytes) ; New
|
||||
*/
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||
pub enum BigInt {
|
||||
Int(Int),
|
||||
BigUInt(ByteVec),
|
||||
|
|
@ -1009,7 +1009,7 @@ impl<C> minicbor::encode::Encode<C> for BigInt {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||
pub enum PlutusData {
|
||||
Constr(Constr<PlutusData>),
|
||||
Map(KeyValuePairs<PlutusData, PlutusData>),
|
||||
|
|
@ -1090,7 +1090,7 @@ impl<C> minicbor::encode::Encode<C> for PlutusData {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||
pub struct Constr<A> {
|
||||
pub tag: u64,
|
||||
pub any_constructor: Option<u64>,
|
||||
|
|
@ -1159,7 +1159,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct ExUnits {
|
||||
#[n(0)]
|
||||
pub mem: u32,
|
||||
|
|
@ -1167,7 +1167,7 @@ pub struct ExUnits {
|
|||
pub steps: u64,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct ExUnitPrices {
|
||||
#[n(0)]
|
||||
mem_price: PositiveInterval,
|
||||
|
|
@ -1176,7 +1176,7 @@ pub struct ExUnitPrices {
|
|||
step_price: PositiveInterval,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
#[cbor(index_only)]
|
||||
pub enum RedeemerTag {
|
||||
#[n(0)]
|
||||
|
|
@ -1189,7 +1189,7 @@ pub enum RedeemerTag {
|
|||
Reward,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct Redeemer {
|
||||
#[n(0)]
|
||||
pub tag: RedeemerTag,
|
||||
|
|
@ -1211,7 +1211,7 @@ pub struct Redeemer {
|
|||
, attributes : bytes
|
||||
] */
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct BootstrapWitness {
|
||||
#[n(0)]
|
||||
pub public_key: ByteVec,
|
||||
|
|
@ -1226,7 +1226,7 @@ pub struct BootstrapWitness {
|
|||
pub attributes: ByteVec,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
#[cbor(map)]
|
||||
pub struct TransactionWitnessSet {
|
||||
#[n(0)]
|
||||
|
|
@ -1248,7 +1248,7 @@ pub struct TransactionWitnessSet {
|
|||
pub redeemer: Option<MaybeIndefArray<Redeemer>>,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
#[cbor(map)]
|
||||
pub struct AlonzoAuxiliaryData {
|
||||
#[n(0)]
|
||||
|
|
@ -1259,7 +1259,7 @@ pub struct AlonzoAuxiliaryData {
|
|||
pub plutus_scripts: Option<MaybeIndefArray<PlutusScript>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||
pub enum Metadatum {
|
||||
Int(Int),
|
||||
Bytes(ByteVec),
|
||||
|
|
@ -1350,7 +1350,7 @@ pub type MetadatumLabel = AnyUInt;
|
|||
|
||||
pub type Metadata = KeyValuePairs<MetadatumLabel, Metadatum>;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum AuxiliaryData {
|
||||
Shelley(Metadata),
|
||||
ShelleyMa {
|
||||
|
|
@ -1417,7 +1417,7 @@ impl<C> minicbor::Encode<C> for AuxiliaryData {
|
|||
|
||||
pub type TransactionIndex = u32;
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct Block {
|
||||
#[n(0)]
|
||||
pub header: Header,
|
||||
|
|
@ -1459,18 +1459,33 @@ pub struct MintedBlock<'b> {
|
|||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug)]
|
||||
pub struct Transaction {
|
||||
pub struct Tx {
|
||||
#[n(0)]
|
||||
transaction_body: TransactionBody,
|
||||
pub transaction_body: TransactionBody,
|
||||
|
||||
#[n(1)]
|
||||
transaction_witness_set: TransactionWitnessSet,
|
||||
pub transaction_witness_set: TransactionWitnessSet,
|
||||
|
||||
#[n(2)]
|
||||
success: bool,
|
||||
pub success: bool,
|
||||
|
||||
#[n(3)]
|
||||
auxiliary_data: Option<AuxiliaryData>,
|
||||
pub auxiliary_data: Option<AuxiliaryData>,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug)]
|
||||
pub struct MintedTx<'b> {
|
||||
#[b(0)]
|
||||
pub transaction_body: KeepRaw<'b, TransactionBody>,
|
||||
|
||||
#[n(1)]
|
||||
pub transaction_witness_set: TransactionWitnessSet,
|
||||
|
||||
#[n(2)]
|
||||
pub success: bool,
|
||||
|
||||
#[n(3)]
|
||||
pub auxiliary_data: Option<KeepRaw<'b, AuxiliaryData>>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ impl Address {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::byron::MintedMainBlock;
|
||||
use crate::byron::MintedBlock;
|
||||
|
||||
type BlockWrapper<'b> = (u16, MintedMainBlock<'b>);
|
||||
type BlockWrapper<'b> = (u16, MintedBlock<'b>);
|
||||
|
||||
const KNOWN_ADDRESSES: &[&str] = &[
|
||||
"DdzFFzCqrht8QHTQXbWy2qoyPaqTN8BjyfKygGmpy9dtot1tvkBfCaVTnR22XCaaDVn3M1U6aiMShoCLzw6VWSwzQKhhJrM3YjYp3wyy",
|
||||
|
|
|
|||
|
|
@ -48,9 +48,9 @@ impl ToHash<32> for KeepRaw<'_, Tx> {
|
|||
mod tests {
|
||||
use pallas_codec::minicbor;
|
||||
|
||||
use crate::{byron::MintedMainBlock, ToHash};
|
||||
use crate::{byron::MintedBlock, ToHash};
|
||||
|
||||
type BlockWrapper<'b> = (u16, MintedMainBlock<'b>);
|
||||
type BlockWrapper<'b> = (u16, MintedBlock<'b>);
|
||||
|
||||
const KNOWN_HASH: &'static str =
|
||||
"5c196e7394ace0449ba5a51c919369699b13896e97432894b4f0354dce8670b6";
|
||||
|
|
|
|||
|
|
@ -48,9 +48,9 @@ impl TxPayload {
|
|||
mod tests {
|
||||
use pallas_codec::minicbor;
|
||||
|
||||
use crate::{byron::MainBlock, ToHash};
|
||||
use crate::{byron::Block, ToHash};
|
||||
|
||||
type BlockWrapper = (u16, MainBlock);
|
||||
type BlockWrapper = (u16, Block);
|
||||
|
||||
#[test]
|
||||
fn known_fee_matches() {
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ pub type Attributes = EmptyMap;
|
|||
|
||||
// Addresses
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AddrDistr {
|
||||
Variant0(StakeholderId),
|
||||
Variant1,
|
||||
|
|
@ -96,7 +96,7 @@ impl minicbor::Encode<()> for AddrDistr {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AddrType {
|
||||
PubKey,
|
||||
Script,
|
||||
|
|
@ -137,7 +137,7 @@ impl<C> minicbor::Encode<C> for AddrType {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AddrAttrProperty {
|
||||
AddrDistr(AddrDistr),
|
||||
Bytes(ByteVec),
|
||||
|
|
@ -187,7 +187,7 @@ impl<C> minicbor::Encode<C> for AddrAttrProperty {
|
|||
|
||||
pub type AddrAttr = OrderPreservingProperties<AddrAttrProperty>;
|
||||
|
||||
#[derive(Debug, Encode, Decode)]
|
||||
#[derive(Debug, Encode, Decode, Clone)]
|
||||
pub struct AddressPayload {
|
||||
#[n(0)]
|
||||
pub root: AddressId,
|
||||
|
|
@ -200,7 +200,7 @@ pub struct AddressPayload {
|
|||
}
|
||||
|
||||
// address = [ #6.24(bytes .cbor ([addressid, addrattr, addrtype])), u64 ]
|
||||
#[derive(Debug, Encode, Decode)]
|
||||
#[derive(Debug, Encode, Decode, Clone)]
|
||||
pub struct Address {
|
||||
#[n(0)]
|
||||
pub payload: CborWrap<AddressPayload>,
|
||||
|
|
@ -212,7 +212,7 @@ pub struct Address {
|
|||
// Transactions
|
||||
|
||||
// txout = [address, u64]
|
||||
#[derive(Debug, Encode, Decode)]
|
||||
#[derive(Debug, Encode, Decode, Clone)]
|
||||
pub struct TxOut {
|
||||
#[n(0)]
|
||||
pub address: Address,
|
||||
|
|
@ -221,7 +221,7 @@ pub struct TxOut {
|
|||
pub amount: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TxIn {
|
||||
// [0, #6.24(bytes .cbor ([txid, u32]))]
|
||||
Variant0(CborWrap<(TxId, u32)>),
|
||||
|
|
@ -269,7 +269,7 @@ impl<C> minicbor::Encode<C> for TxIn {
|
|||
}
|
||||
|
||||
// tx = [[+ txin], [+ txout], attributes]
|
||||
#[derive(Debug, Encode, Decode)]
|
||||
#[derive(Debug, Encode, Decode, Clone)]
|
||||
pub struct Tx {
|
||||
#[n(0)]
|
||||
pub inputs: MaybeIndefArray<TxIn>,
|
||||
|
|
@ -287,7 +287,7 @@ pub type TxProof = (u32, ByronHash, ByronHash);
|
|||
pub type ValidatorScript = (u16, ByteVec);
|
||||
pub type RedeemerScript = (u16, ByteVec);
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Twit {
|
||||
// [0, #6.24(bytes .cbor ([pubkey, signature]))]
|
||||
PkWitness(CborWrap<(PubKey, Signature)>),
|
||||
|
|
@ -841,7 +841,6 @@ pub struct BlockHead {
|
|||
pub extra_data: BlockHeadEx,
|
||||
}
|
||||
|
||||
// [tx, [* twit]]
|
||||
#[derive(Debug, Encode, Decode)]
|
||||
pub struct TxPayload {
|
||||
#[n(0)]
|
||||
|
|
@ -851,6 +850,15 @@ pub struct TxPayload {
|
|||
pub witness: MaybeIndefArray<Twit>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Encode, Decode, Clone)]
|
||||
pub struct MintedTxPayload<'b> {
|
||||
#[b(0)]
|
||||
pub transaction: KeepRaw<'b, Tx>,
|
||||
|
||||
#[n(1)]
|
||||
pub witness: MaybeIndefArray<Twit>,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug)]
|
||||
pub struct BlockBody {
|
||||
#[n(0)]
|
||||
|
|
@ -866,6 +874,21 @@ pub struct BlockBody {
|
|||
pub upd_payload: Up,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug)]
|
||||
pub struct MintedBlockBody<'b> {
|
||||
#[b(0)]
|
||||
pub tx_payload: MaybeIndefArray<MintedTxPayload<'b>>,
|
||||
|
||||
#[b(1)]
|
||||
pub ssc_payload: Ssc,
|
||||
|
||||
#[b(2)]
|
||||
pub dlg_payload: MaybeIndefArray<Dlg>,
|
||||
|
||||
#[b(3)]
|
||||
pub upd_payload: Up,
|
||||
}
|
||||
|
||||
// Epoch Boundary Blocks
|
||||
|
||||
#[derive(Encode, Decode, Debug)]
|
||||
|
|
@ -896,7 +919,7 @@ pub struct EbbHead {
|
|||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug)]
|
||||
pub struct MainBlock {
|
||||
pub struct Block {
|
||||
#[n(0)]
|
||||
pub header: BlockHead,
|
||||
|
||||
|
|
@ -908,12 +931,12 @@ pub struct MainBlock {
|
|||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug)]
|
||||
pub struct MintedMainBlock<'b> {
|
||||
pub struct MintedBlock<'b> {
|
||||
#[b(0)]
|
||||
pub header: KeepRaw<'b, BlockHead>,
|
||||
|
||||
#[n(1)]
|
||||
pub body: BlockBody,
|
||||
#[b(1)]
|
||||
pub body: MintedBlockBody<'b>,
|
||||
|
||||
#[n(2)]
|
||||
pub extra: MaybeIndefArray<Attributes>,
|
||||
|
|
@ -933,7 +956,7 @@ pub struct EbBlock {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{BlockHead, EbBlock, MintedMainBlock};
|
||||
use super::{BlockHead, EbBlock, MintedBlock};
|
||||
use pallas_codec::minicbor::{self, to_vec};
|
||||
|
||||
#[test]
|
||||
|
|
@ -958,7 +981,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn main_block_isomorphic_decoding_encoding() {
|
||||
type BlockWrapper<'b> = (u16, MintedMainBlock<'b>);
|
||||
type BlockWrapper<'b> = (u16, MintedBlock<'b>);
|
||||
|
||||
let test_blocks = vec![
|
||||
//include_str!("../../../test_data/genesis.block"),
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@ impl EbbHead {
|
|||
mod tests {
|
||||
use pallas_codec::minicbor;
|
||||
|
||||
use crate::byron::MainBlock;
|
||||
use crate::byron::Block;
|
||||
|
||||
type BlockWrapper = (u16, MainBlock);
|
||||
type BlockWrapper = (u16, Block);
|
||||
|
||||
#[test]
|
||||
fn knwon_slot_matches() {
|
||||
|
|
|
|||
|
|
@ -23,15 +23,6 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum Era {
|
||||
Byron,
|
||||
Shelley,
|
||||
Allegra, // time-locks
|
||||
Mary, // multi-assets
|
||||
Alonzo, // smart-contracts
|
||||
}
|
||||
|
||||
#[cfg(feature = "json")]
|
||||
pub trait ToCanonicalJson {
|
||||
fn to_json(&self) -> serde_json::Value;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,5 @@ mod framework;
|
|||
|
||||
pub mod alonzo;
|
||||
pub mod byron;
|
||||
pub mod probing;
|
||||
|
||||
pub use framework::*;
|
||||
|
|
|
|||
20
pallas-traverse/Cargo.toml
Normal file
20
pallas-traverse/Cargo.toml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
[package]
|
||||
name = "pallas-traverse"
|
||||
description = "Utilities to traverse over multi-era block data"
|
||||
version = "0.11.0-alpha.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/txpipe/pallas"
|
||||
homepage = "https://github.com/txpipe/pallas"
|
||||
documentation = "https://docs.rs/pallas-traverse"
|
||||
license = "Apache-2.0"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
"Santiago Carmuega <santiago@carmuega.me>",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
pallas-primitives = { version = "0.11.0-alpha.0", path = "../pallas-primitives" }
|
||||
pallas-crypto = { version = "0.11.0-alpha.0", path = "../pallas-crypto" }
|
||||
pallas-codec = { version = "0.11.0-alpha.0", path = "../pallas-codec" }
|
||||
hex = "0.4.3"
|
||||
thiserror = "1.0.31"
|
||||
63
pallas-traverse/src/block.rs
Normal file
63
pallas-traverse/src/block.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
use pallas_codec::minicbor;
|
||||
use pallas_crypto::hash::Hash;
|
||||
use pallas_primitives::{alonzo, byron, ToHash};
|
||||
|
||||
use crate::{probe, Era, Error, MultiEraBlock};
|
||||
|
||||
type BlockWrapper<T> = (u16, T);
|
||||
|
||||
impl<'b> MultiEraBlock<'b> {
|
||||
pub fn from_epoch_boundary(block: byron::EbBlock) -> Self {
|
||||
Self::EpochBoundary(Box::new(block))
|
||||
}
|
||||
|
||||
pub fn from_byron(block: byron::MintedBlock<'b>) -> Self {
|
||||
Self::Byron(Box::new(block))
|
||||
}
|
||||
|
||||
pub fn from_alonzo_compatible(block: alonzo::MintedBlock<'b>) -> Self {
|
||||
Self::AlonzoCompatible(Box::new(block))
|
||||
}
|
||||
|
||||
pub fn decode(cbor: &'b [u8]) -> Result<MultiEraBlock<'b>, Error> {
|
||||
match probe::block_era(cbor) {
|
||||
probe::Outcome::EpochBoundary => {
|
||||
let (_, block): BlockWrapper<byron::EbBlock> =
|
||||
minicbor::decode(cbor).map_err(Error::invalid_cbor)?;
|
||||
|
||||
Ok(MultiEraBlock::from_epoch_boundary(block))
|
||||
}
|
||||
probe::Outcome::Matched(era) => match era {
|
||||
Era::Byron => {
|
||||
let (_, block): BlockWrapper<byron::MintedBlock> =
|
||||
minicbor::decode(cbor).map_err(Error::invalid_cbor)?;
|
||||
|
||||
Ok(Self::from_byron(block))
|
||||
}
|
||||
Era::Shelley | Era::Allegra | Era::Mary | Era::Alonzo => {
|
||||
let (_, block): BlockWrapper<alonzo::MintedBlock> =
|
||||
minicbor::decode(cbor).map_err(Error::invalid_cbor)?;
|
||||
|
||||
Ok(Self::from_alonzo_compatible(block))
|
||||
}
|
||||
},
|
||||
probe::Outcome::Inconclusive => Err(Error::unknown_cbor(cbor)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hash(&self) -> Hash<32> {
|
||||
match self {
|
||||
MultiEraBlock::EpochBoundary(x) => x.header.to_hash(),
|
||||
MultiEraBlock::AlonzoCompatible(x) => x.header.to_hash(),
|
||||
MultiEraBlock::Byron(x) => x.header.to_hash(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn slot(&self) -> u64 {
|
||||
match self {
|
||||
MultiEraBlock::EpochBoundary(x) => x.header.to_abs_slot(),
|
||||
MultiEraBlock::AlonzoCompatible(x) => x.header.header_body.slot,
|
||||
MultiEraBlock::Byron(x) => x.header.consensus_data.0.to_abs_slot(),
|
||||
}
|
||||
}
|
||||
}
|
||||
96
pallas-traverse/src/iter.rs
Normal file
96
pallas-traverse/src/iter.rs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
//! Iterate over block data
|
||||
|
||||
use pallas_primitives::{alonzo, byron};
|
||||
|
||||
use crate::{MultiEraBlock, MultiEraTx};
|
||||
|
||||
fn clone_alonzo_tx_at<'b>(
|
||||
block: &'b alonzo::MintedBlock,
|
||||
index: usize,
|
||||
) -> Option<alonzo::MintedTx<'b>> {
|
||||
let transaction_body = block.transaction_bodies.get(index).cloned()?;
|
||||
let transaction_witness_set = block.transaction_witness_sets.get(index).cloned()?;
|
||||
let success = block
|
||||
.invalid_transactions
|
||||
.as_ref()?
|
||||
.contains(&(index as u32));
|
||||
|
||||
let auxiliary_data = block
|
||||
.auxiliary_data_set
|
||||
.iter()
|
||||
.find_map(|(idx, val)| {
|
||||
if idx.eq(&(index as u32)) {
|
||||
Some(val)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.cloned();
|
||||
|
||||
Some(alonzo::MintedTx {
|
||||
transaction_body,
|
||||
transaction_witness_set,
|
||||
success,
|
||||
auxiliary_data,
|
||||
})
|
||||
}
|
||||
|
||||
fn clone_byron_tx_at<'b>(
|
||||
block: &'b byron::MintedBlock,
|
||||
index: usize,
|
||||
) -> Option<byron::MintedTxPayload<'b>> {
|
||||
block.body.tx_payload.get(index).cloned()
|
||||
}
|
||||
|
||||
pub struct TxIter<'b> {
|
||||
block: &'b MultiEraBlock<'b>,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl<'b> Iterator for TxIter<'b> {
|
||||
type Item = MultiEraTx<'b>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let tx = match self.block {
|
||||
MultiEraBlock::EpochBoundary(_) => None,
|
||||
MultiEraBlock::AlonzoCompatible(x) => {
|
||||
clone_alonzo_tx_at(x, self.index).map(MultiEraTx::from_alonzo_compatible)
|
||||
}
|
||||
MultiEraBlock::Byron(x) => clone_byron_tx_at(x, self.index).map(MultiEraTx::from_byron),
|
||||
}?;
|
||||
|
||||
self.index += 1;
|
||||
Some(tx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'b> MultiEraBlock<'b> {
|
||||
pub fn tx_iter(&'b self) -> TxIter<'b> {
|
||||
TxIter {
|
||||
index: 0,
|
||||
block: self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_iteration() {
|
||||
let blocks = vec![
|
||||
(include_str!("../../test_data/byron2.block"), 2usize),
|
||||
(include_str!("../../test_data/shelley1.block"), 0),
|
||||
(include_str!("../../test_data/mary1.block"), 0),
|
||||
(include_str!("../../test_data/allegra1.block"), 0),
|
||||
(include_str!("../../test_data/alonzo1.block"), 5),
|
||||
];
|
||||
|
||||
for (block_str, tx_count) in blocks.into_iter() {
|
||||
let cbor = hex::decode(block_str).expect("invalid hex");
|
||||
let block = MultiEraBlock::decode(&cbor).expect("invalid cbor");
|
||||
assert_eq!(block.tx_iter().count(), tx_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
54
pallas-traverse/src/lib.rs
Normal file
54
pallas-traverse/src/lib.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
//! Utilities to traverse over multi-era block data
|
||||
use std::fmt::Display;
|
||||
|
||||
use pallas_primitives::{alonzo, byron};
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod block;
|
||||
pub mod iter;
|
||||
pub mod probe;
|
||||
pub mod tx;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[non_exhaustive]
|
||||
pub enum Era {
|
||||
Byron,
|
||||
Shelley,
|
||||
Allegra, // time-locks
|
||||
Mary, // multi-assets
|
||||
Alonzo, // smart-contracts
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum MultiEraTx<'b> {
|
||||
AlonzoCompatible(Box<alonzo::MintedTx<'b>>),
|
||||
Byron(Box<byron::MintedTxPayload<'b>>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum MultiEraBlock<'b> {
|
||||
EpochBoundary(Box<byron::EbBlock>),
|
||||
AlonzoCompatible(Box<alonzo::MintedBlock<'b>>),
|
||||
Byron(Box<byron::MintedBlock<'b>>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Invalid CBOR structure: {0}")]
|
||||
InvalidCbor(String),
|
||||
|
||||
#[error("Unknown CBOR structure: {0}")]
|
||||
UnknownCbor(String),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn invalid_cbor(error: impl Display) -> Self {
|
||||
Error::InvalidCbor(format!("{}", error))
|
||||
}
|
||||
|
||||
pub fn unknown_cbor(bytes: &[u8]) -> Self {
|
||||
Error::UnknownCbor(hex::encode(bytes))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
//! Heuristics for detecting cbor content without decoding
|
||||
//! Lightweight inspection of block data without full CBOR decoding
|
||||
|
||||
use pallas_codec::minicbor::decode::{Token, Tokenizer};
|
||||
|
||||
|
|
@ -12,9 +12,9 @@ pub enum Outcome {
|
|||
}
|
||||
|
||||
// Executes a very lightweight inspection of the initial tokens of the CBOR
|
||||
// payload and infers with a certain degree of confidence the type of Cardano
|
||||
// structure within.
|
||||
pub fn probe_block_cbor_era(cbor: &[u8]) -> Outcome {
|
||||
// block payload to extract the tag of the block wrapper which defines the era
|
||||
// of the contained bytes.
|
||||
pub fn block_era(cbor: &[u8]) -> Outcome {
|
||||
let mut tokenizer = Tokenizer::new(cbor);
|
||||
|
||||
if !matches!(tokenizer.next(), Some(Ok(Token::Array(2)))) {
|
||||
|
|
@ -44,7 +44,7 @@ mod tests {
|
|||
let block_str = include_str!("../../test_data/genesis.block");
|
||||
let bytes = hex::decode(block_str).unwrap();
|
||||
|
||||
let inference = probe_block_cbor_era(bytes.as_slice());
|
||||
let inference = block_era(bytes.as_slice());
|
||||
|
||||
assert!(matches!(inference, Outcome::EpochBoundary));
|
||||
}
|
||||
|
|
@ -54,7 +54,7 @@ mod tests {
|
|||
let block_str = include_str!("../../test_data/byron1.block");
|
||||
let bytes = hex::decode(block_str).unwrap();
|
||||
|
||||
let inference = probe_block_cbor_era(bytes.as_slice());
|
||||
let inference = block_era(bytes.as_slice());
|
||||
|
||||
assert!(matches!(inference, Outcome::Matched(Era::Byron)));
|
||||
}
|
||||
|
|
@ -64,7 +64,7 @@ mod tests {
|
|||
let block_str = include_str!("../../test_data/shelley1.block");
|
||||
let bytes = hex::decode(block_str).unwrap();
|
||||
|
||||
let inference = probe_block_cbor_era(bytes.as_slice());
|
||||
let inference = block_era(bytes.as_slice());
|
||||
|
||||
assert!(matches!(inference, Outcome::Matched(Era::Shelley)));
|
||||
}
|
||||
|
|
@ -74,7 +74,7 @@ mod tests {
|
|||
let block_str = include_str!("../../test_data/allegra1.block");
|
||||
let bytes = hex::decode(block_str).unwrap();
|
||||
|
||||
let inference = probe_block_cbor_era(bytes.as_slice());
|
||||
let inference = block_era(bytes.as_slice());
|
||||
|
||||
assert!(matches!(inference, Outcome::Matched(Era::Allegra)));
|
||||
}
|
||||
|
|
@ -84,7 +84,7 @@ mod tests {
|
|||
let block_str = include_str!("../../test_data/mary1.block");
|
||||
let bytes = hex::decode(block_str).unwrap();
|
||||
|
||||
let inference = probe_block_cbor_era(bytes.as_slice());
|
||||
let inference = block_era(bytes.as_slice());
|
||||
|
||||
assert!(matches!(inference, Outcome::Matched(Era::Mary)));
|
||||
}
|
||||
|
|
@ -94,7 +94,7 @@ mod tests {
|
|||
let block_str = include_str!("../../test_data/alonzo1.block");
|
||||
let bytes = hex::decode(block_str).unwrap();
|
||||
|
||||
let inference = probe_block_cbor_era(bytes.as_slice());
|
||||
let inference = block_era(bytes.as_slice());
|
||||
|
||||
assert!(matches!(inference, Outcome::Matched(Era::Alonzo)));
|
||||
}
|
||||
13
pallas-traverse/src/tx.rs
Normal file
13
pallas-traverse/src/tx.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
use pallas_primitives::{alonzo, byron};
|
||||
|
||||
use crate::MultiEraTx;
|
||||
|
||||
impl<'b> MultiEraTx<'b> {
|
||||
pub fn from_byron(tx: byron::MintedTxPayload<'b>) -> Self {
|
||||
Self::Byron(Box::new(tx))
|
||||
}
|
||||
|
||||
pub fn from_alonzo_compatible(tx: alonzo::MintedTx<'b>) -> Self {
|
||||
Self::AlonzoCompatible(Box::new(tx))
|
||||
}
|
||||
}
|
||||
|
|
@ -16,5 +16,6 @@ authors = [
|
|||
pallas-multiplexer = { version = "0.11.0-alpha.0", path = "../pallas-multiplexer/" }
|
||||
pallas-miniprotocols = { version = "0.11.0-alpha.0", path = "../pallas-miniprotocols/" }
|
||||
pallas-primitives = { version = "0.11.0-alpha.0", path = "../pallas-primitives/" }
|
||||
pallas-traverse = { version = "0.11.0-alpha.0", path = "../pallas-traverse/" }
|
||||
pallas-crypto = { version = "0.11.0-alpha.0", path = "../pallas-crypto/" }
|
||||
pallas-codec = { version = "0.11.0-alpha.0", path = "../pallas-codec/" }
|
||||
|
|
|
|||
|
|
@ -2,3 +2,6 @@
|
|||
|
||||
#[doc(inline)]
|
||||
pub use pallas_primitives as primitives;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use pallas_traverse as traverse;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue