[Announcement] Smart Contract Diagram Generator for NEAR by PrimeLab!

Smart Contract Diagram Generator for NEAR by PrimeLab!**

Cargo utility that allows developers to generate visualizations depicting data flows and methods within their NEAR smart contracts. (Rust)

Contributors:
PrimeLab Blockchain Team

GitHub: GitHub - Web3-Diagram-Generator

Our Smart Contract Diagram Generator enables developers to quickly visualize methods and understand how their contracts works.

Why (is this needed)

  • Decreases on-ramping time for new developers.
  • Provides developers a visual representation of NEAR’s smart contract structure.
  • Helpful for visualizing:
    • Contract Data Flows
    • Functions and their types
  • Visualizing code is a futuristic way of reading code (ex: VS Studio) and can be implemented into VsCode or other IDEs

Who (would this benefit)

  • Blockchain/Rust developers
  • Consumers (Seeking to understand more about the contract they’re using)
  • Contract Owners
  • Technical Writers
  • Smart Contract Auditors
  • Educational Content & Programs

How (does this achieve a solution):
Below the diagram shows the basic user flow of this tool

Smart Contract Developers can use this Utility to generate diagrams in a multitude of formats. The resulting diagrams enable anyone to quickly and effectively understand how the Smart Contract is working.

Each function has the corresponding visual shape or connection so that the user can understand the function type(event, pub view function, etc.)

Diagram Legend

To differentiate between various functions, the diagram utility scans the AST tree of the contract source code and collects all the contract information into known Rust structures.

Shape/Color function mappings:

  • The circle is a view function inside the contract, and the blue color indicates the modifier of the functioning blue means public
  • Hexagon **** is a mutable function, blue is for public functions also
  • The rectangle is a process function that is not returning value and they are grayed if private
  • Triangle is for events
  • Trait impl functions are circles in teal color
  • Hexagon tealed as mutable trait functions
  • Circles in green for payable functions
  • Orange hexagon for initializer functions
  • Direct connections are for regular functions just simple fn function inside the smart contract, they are not part of trait or event.

Example

#[near_bindgen]
impl Contract {
    //Direct connection function for Contract
    pub fn add(&mut self, amount: u64) {
        if amount == 2 {
            self.count = SomeStruct::add_two(self.count);
        }
        self.add_amount(amount);
    }
}
  • Trait connections are for trait implementation functions(impl trait) or functions that are out of contract structure scope

Example:

#[near_bindgen]
impl Contract {
}

pub trait SomeTrait {
    fn view_function(&self) -> u64;
}

#[near_bindgen]
impl SomeTrait for Contract {
    //Trait connection function
    fn view_function(&self) -> u64 {
        self.count * 10
    }
}
  • Emission connections are for events, functions that are part of event standards.

Example:

impl NearEvent {
    //Emittion connection type function
    pub fn new_171(version: String, event_kind: Nep171EventKind) -> Self {
        NearEvent::Nep171(Nep171Event {
            version,
            event_kind,
        })
    }

    //Emittion connection type function
    pub fn new_171_v1(event_kind: Nep171EventKind) -> Self {
        NearEvent::new_171("1.0.0".to_string(), event_kind)
    }
    
    //Emittion connection type function
    pub fn nft_burn(data: Vec<NftBurnData>) -> Self {
        NearEvent::new_171_v1(Nep171EventKind::NftBurn(data))
    }
}

Components:

The diagram utility consists of 3 modules.

Module 1 (scanner_syn) is for scanning the AST tree of the smart contract and differentiating functions

  • Scans contracts
  • Builds hierarchy trees

Module 2 (markdown_api) is for generating markdown code for diagram js client

  • Parses hierarchy trees into a linked list of nodes & connections
  • Parses linked list into a structure containing the mermaid markdown compatible string schema
  • Allows for additional custom syntaxes to be utilized by inserting a syntax definition that fits the expected trait

Module 3 is a cargo plugin for running utility commands over smart contracts.

Contribution:

Adding custom syntax:

To include your own custom syntax you need to write a definition file that contains a struct that implements the CoreSyntaxFunctions trait and pass in the syntax name to the call.

Example Syntax Definition:

/// The various different shapes enabled by this API.
#[derive(EnumProperty, Debug)]
pub enum Shape {
    #[strum(props(Left = "((", Right = "))"))]
    Circle,
    #[strum(props(Left = "{{", Right = "}}"))]
    Hexagon,
    #[strum(props(Left = "[", Right = "]"))]
    Rectangle,
    #[strum(props(Left = ">", Right = "]"))]
    Flag,
}

/// The various different line types enabled by this API.
#[derive(EnumProperty, Debug)]
pub enum LineType {
    #[strum(props(Complete = "--", Addition = "-"))]
    Solid,
    #[strum(props(Left = "-.", Right = ".-", Addition = "."))]
    Dashed,
}

/// The various different arrow types enabled by this API.
#[derive(AsRefStr, EnumProperty, Debug, Clone, Copy)]
pub enum ArrowType {
    #[strum(props(Left = "<", Right = ">"))]
    Standard,
    #[strum(props(Left = "x", Right = "x"))]
    X,
    #[strum(props(Left = "o", Right = "o"))]
    O,
}

#[derive(AsRefStr, Debug)]
pub enum ArrowDirection {
    BiDirectional,
    Left,
    Right,
    None,
}

#[derive(EnumProperty, EnumAsInner, Debug)]
pub enum ObjectConfig<'a> {
    NodeConfig(NodeConfig<'a>),
    ConnectionConfig(ConnectionConfig),
}

#[derive(Debug)]
pub struct NodeConfig<'a> {
    pub id: &'a str,
    pub class: Option<&'a str>,
    pub shape: Shape,
    pub inner_text: &'a str,
}

#[derive(Debug)]
pub struct ConnectionConfig {
    pub line_type: LineType,
    pub arrow_type: ArrowType,
    pub arrow_direction: ArrowDirection,
    pub extra_length_num: Option<u8>,
}

/// This is the root struct for an individual flow chart.
pub struct FlowChart {
    /// This is the data location of the string data for the markdown
    data: String,
}

impl FlowChart {
    /// Creates a [Mermaid.js Dotted/Dashed Line](https://mermaid-js.github.io/mermaid/#/flowchart?id=dotted-link) with the supplied attributes & appends it to the current data of the flow chart struct (i.e. `self.data`).
    ///
    /// # Arguments
    ///
    /// * `extra_length_num` - An optional amount of additional flags to increase line length
    fn add_dashed_line(
        &mut self,
        extra_length_num: Option<u8>,
    ) {
        // Push the left half of the dashed line flag
        self.data
            .push_str(LineType::Dashed.get_str("Left").unwrap());

        // Check to see if an additional length was requested
        if let Some(extra_length_num) = extra_length_num {
            // Range over `extra_length_num` to add the appropriate number of length additions
            for _ in 0..extra_length_num {
                // Add in a `.`
                self.data
                    .push_str(LineType::Dashed.get_str("Addition").unwrap());
            }
        }

        // Push the right half of the dashed line flag
        self.data
            .push_str(LineType::Dashed.get_str("Right").unwrap());
    }

    /// Creates a [Mermaid.js Solid Line](https://mermaid-js.github.io/mermaid/#/flowchart?id=a-link-with-arrow-head) with the supplied attributes & appends it to the current data of the flow chart struct (i.e. `self.data`).
    ///
    /// # Arguments
    ///
    /// * `extra_length_num` - An optional amount of additional flags to increase line length
    fn add_solid_line(
        &mut self,
        extra_length_num: Option<u8>,
    ) {
        // Push the main portion of the solid line flag
        self.data.push_str(LineType::Solid.get_str("Main").unwrap());

        // Check to see if an additional length was requested
        if let Some(extra_length_num) = extra_length_num {
            // Range over `extra_length_num` to add the appropriate number of length additions
            for _ in 0..extra_length_num {
                // Add in a `-`
                self.data
                    .push_str(LineType::Solid.get_str("Addition").unwrap());
            }
        }
    }

    /// Creates a [Mermaid.js Connection Line with no arrow](https://mermaid-js.github.io/mermaid/#/flowchart?id=links-between-nodes) with the supplied attributes & appends it to the current data of the flow chart struct (i.e. `self.data`).
    ///
    /// # Arguments
    ///
    /// * `line_type` - The enum representation of the line type you want
    /// * `extra_length_num` - An optional amount of additional flags to increase line length
    fn add_line(
        &mut self,
        line_type: LineType,
        extra_length_num: Option<u8>,
    ) {
        match line_type {
            LineType::Solid => self.add_solid_line(extra_length_num),
            LineType::Dashed => self.add_dashed_line(extra_length_num),
        }
    }

    /// Creates a [Mermaid.js Arrow](https://mermaid-js.github.io/mermaid/#/flowchart?id=new-arrow-types) with the supplied attributes & appends it to the current data of the flow chart struct (i.e. `self.data`).
    ///
    /// # Arguments
    ///
    /// * `arrow_type` - The enum representation of the arrow type you want
    /// * `arrow_direction` - The enum representation of the direction you want the arrow to
    fn add_arrow(
        &mut self,
        arrow_type: ArrowType,
        arrow_direction: ArrowDirection,
    ) {
        // Get the `arrow_direction` as a str to use as the key for the `ArrowType` enum property to then add the correct arrow flag
        self.data
            .push_str(arrow_type.get_str(arrow_direction.as_ref()).unwrap())
    }

    fn get_shape_from_node(
        &self,
        node: &Node,
    ) -> Shape {
        match node.action {
            ActionType::Mutation => Shape::Hexagon,
            ActionType::View => Shape::Circle,
            ActionType::Process => Shape::Rectangle,
            ActionType::Event => Shape::Flag,
        }
    }

    fn get_line_and_arrow_type_from_connection(
        &self,
        connection: &Connection,
    ) -> (LineType, ArrowType, ArrowDirection) {
        match connection.connection_type {
            ConnectionType::DirectConnection => {
                (LineType::Solid, ArrowType::Standard, ArrowDirection::Right)
            }
            ConnectionType::CrossContractConnection => {
                (LineType::Solid, ArrowType::Standard, ArrowDirection::Right)
            }
            ConnectionType::Emission => {
                (LineType::Solid, ArrowType::Standard, ArrowDirection::Right)
            }
        }
    }
}

impl CoreSyntaxFunctions for FlowChart {
    /// Returns a `FlowChart` struct to allow you to build the necessary markdown text.
    ///
    /// # Arguments
    ///
    /// * `direction` - The enum representation of the flow direction of the diagram
    ///
    /// # Examples
    ///
    /// ```
    /// let mut flow_chart = FlowChart::new(FlowDirection::TD);
    /// ```
    fn new(direction: FlowDirection) -> Self {
        // Instantiate the starting point for the diagram schema
        let mut schema_root = "flowchart ".to_string();

        // Add in `direction`
        schema_root.push_str(direction.as_ref());

        // Instantiate `FlowChart`
        let mut result = FlowChart { data: schema_root };

        // Add a new line
        result.add_linebreak(None);

        result
    }

    /// Appends a linebreak & the preceding whitespace to the current data of the flow chart struct (i.e. `self.data`).
    ///
    /// # Arguments
    ///
    /// * `num_of_indents` - Optional number of indents to insert once the new line is added (default it 1)
    ///
    /// # Examples
    ///
    /// ```
    /// let mut flow_chart = FlowChart::new(FlowDirection::TD);
    ///
    /// flow_chart.add_linebreak(None);
    /// ```
    fn add_linebreak(
        &mut self,
        num_of_indents: Option<u8>,
    ) {
        // Get the number of indents to use
        let number_of_indents = num_of_indents.unwrap_or(1);

        // Add the new line
        self.data += "\n";

        // Range over `number_of_indents` to add the appropriate number of tabs
        for _ in 0..number_of_indents {
            // Add in a tab
            self.data += "\t";
        }
    }

    /// Creates a [Mermaid.js Node](https://mermaid-js.github.io/mermaid/#/flowchart?id=a-node-default) with the supplied attributes & appends it to the current data of the flow chart struct (i.e. `self.data`).
    ///
    /// # Arguments
    ///
    /// * `id` - The ID that will be assigned to this node
    /// * `class` - An optional class name to assign to the node
    /// * `shape` -  The shape of the node
    /// * `inner_text` - The text to be displayed within the node
    ///
    /// # Examples
    ///
    /// ```
    /// let mut flow_chart = FlowChart::new(FlowDirection::TD);
    ///
    /// let node_config =  SyntaxConfigFile::FlowChart(ConfigFile::NodeConfig(NodeConfig {
    ///   id: "A",
    ///   class: None,
    ///   shape: Shape::Circle,
    ///   inner_text: "inner text",
    /// }));
    ///
    /// flow_chart.add_node(node_config);
    /// ```
    fn add_node(
        &mut self,
        node_config: SyntaxConfigFile,
    ) {
        let node_config: NodeConfig = node_config
            .into_flow_chart()
            .unwrap()
            .into_node_config()
            .unwrap();

        // Push the ID
        self.data.push_str(node_config.id);

        // Push the left shape flag
        self.data
            .push_str(node_config.shape.get_str("Left").unwrap());

        // Push the inner text
        self.data.push_str(node_config.inner_text);

        // Push the left shape flag
        self.data
            .push_str(node_config.shape.get_str("Right").unwrap());

        // If a class name was passed push it to `self.data`
        if let Some(class) = node_config.class {
            self.data.push_str(":::");
            self.data.push_str(class);
        }
    }

    fn build_node_config<'a>(
        &self,
        node: &'a Node,
        id: Option<&'a str>,
    ) -> SyntaxConfigFile<'a> {
        SyntaxConfigFile::FlowChart(ObjectConfig::NodeConfig(NodeConfig {
            id: id.unwrap(),
            class: Some(node.scope.as_ref()),
            shape: self.get_shape_from_node(&node),
            inner_text: &node.name,
        }))
    }

    fn build_connection_config<'a>(
        &self,
        connection: &'a Connection,
        extra_length_num: Option<u8>,
    ) -> SyntaxConfigFile<'a> {
        let (line_type, arrow_type, arrow_direction) =
            self.get_line_and_arrow_type_from_connection(connection);

        SyntaxConfigFile::FlowChart(ObjectConfig::ConnectionConfig(ConnectionConfig {
            line_type,
            arrow_type,
            arrow_direction,
            extra_length_num,
        }))
    }

    /// Creates a [Mermaid.js Connection](https://mermaid-js.github.io/mermaid/#/flowchart?id=links-between-nodes) with the supplied attributes & appends it to the current data of the flow chart struct (i.e. `self.data`).
    ///
    /// # Arguments
    /// TODO:
    /// * `line_type` - The enum representation of the type of line you want
    /// * `arrow_type` - The enum representation of the type of arrow you want
    /// * `arrow_direction` -  The enum representation of the direction you want the arrows to point
    /// * `extra_length_num` - An optional amount of additional flags to increase line length
    ///
    /// # Examples
    ///
    /// ```
    /// let mut flow_chart = FlowChart::new(FlowDirection::TD);
    ///
    /// let node_config =  SyntaxConfigFile::FlowChart(ConfigFile::NodeConfig(NodeConfig {
    ///   id: "A",
    ///   class: None,
    ///   shape: Shape::Circle,
    ///   inner_text: "inner text",
    /// }));
    ///
    /// let connection_config = SyntaxConfigFile::FlowChart(ConfigFile::ConnectionConfig(ConnectionConfig {
    ///   line_type: LineType::Dashed,
    /// 	arrow_type: ArrowType::Standard,
    /// 	arrow_direction: ArrowDirection::Right,
    /// 	extra_length_num: None,
    /// }));
    ///
    /// flow_chart.add_node(node_config);
    /// flow_chart.add_connection(connection_config);
    /// ```
    fn add_connection(
        &mut self,
        connection_config: SyntaxConfigFile,
    ) {
        let connection_config: ConnectionConfig = connection_config
            .into_flow_chart()
            .unwrap()
            .into_connection_config()
            .unwrap();

        // Push a preceding space
        self.data.push_str(" ");

        // Depending on the arrow direction wanted make calls to `self.add_arrow` & `self.add_line`
        match connection_config.arrow_direction {
            ArrowDirection::BiDirectional => {
                self.add_arrow(connection_config.arrow_type, ArrowDirection::Left);
                self.add_line(
                    connection_config.line_type,
                    connection_config.extra_length_num,
                );
                self.add_arrow(connection_config.arrow_type, ArrowDirection::Right)
            }
            ArrowDirection::Left => {
                self.add_arrow(
                    connection_config.arrow_type,
                    connection_config.arrow_direction,
                );
                self.add_line(
                    connection_config.line_type,
                    connection_config.extra_length_num,
                )
            }
            ArrowDirection::Right => {
                self.add_line(
                    connection_config.line_type,
                    connection_config.extra_length_num,
                );
                self.add_arrow(
                    connection_config.arrow_type,
                    connection_config.arrow_direction,
                )
            }
            ArrowDirection::None => {
                self.add_line(
                    connection_config.line_type,
                    connection_config.extra_length_num,
                );
            }
        }

        // Push a trailing space
        self.data.push_str(" ");
    }

    fn return_schema(self) -> String {
        self.data
    }
8 Likes

We tried this but have been unable to get it to work due to a signature issue when installing mermaid-cli.

==> Pouring mermaid-cli–8.11.0.arm64_big_sur.bottle.tar.gz
Error: Failed applying an ad-hoc signature to…

1 Like

It might be an error after updating to bigsur.

See if this solution works for you @blockimperiumdao :slight_smile: MacBook Air M1 Big Sur 11.2 brew update error · Discussion #673 · Homebrew/discussions · GitHub