ernestognw / solidity-mermaid

A Solidity AST processor for transpiling Solidity smart contracts into Github's Mermaid.js language for diagramming.
https://www.npmjs.com/package/solidity-mermaid
MIT License
26 stars 0 forks source link
mermaid solidity

Welcome to solidity-mermaid 👋

Version Documentation Maintenance License: MIT

A Solidity AST parser that allows to convert smart contracts into Github's Mermaid.js language for diagramming.

Solidity is an object-oriented, high-level language for implementing smart contracts on top of the Ethereum Virtual Machine, while Mermaid is a Javascript library for diagramming that includes support for Class Diagrams.

This package aims to be a tool to produce Mermaid definitions from Solidity code, which can be useful for high-level representations, usefulf for audits and security assesment or just putting them on your generated docs. See solidity-docgen.

Take for example the following Solidity code:

// contracts/GameItem.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract GameItem is ERC721URIStorage {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    constructor() ERC721("GameItem", "ITM") {}

    function awardItem(address player, string memory tokenURI)
        public
        returns (uint256)
    {
        uint256 newItemId = _tokenIds.current();
        _mint(player, newItemId);
        _setTokenURI(newItemId, tokenURI);

        _tokenIds.increment();
        return newItemId;
    }
}

It will output the following representation:

classDiagram
  %% 216:471:12
  class GameItem {
    <<Contract>>
    +constructor()
    +awardItem(address player, string memory tokenURI): (uint256)
  }

  GameItem --|> ERC721URIStorage

  %% 248:1623:3
  class ERC721URIStorage {
    <<Contract>>
    +tokenURI(uint256 tokenId): (string memory)
    ~_setTokenURI(uint256 tokenId, string memory _tokenURI)
    ~_burn(uint256 tokenId)
  }

  ERC721URIStorage --|> ERC721

  %% 628:16327:0
  class ERC721 {
    <<Contract>>
    +constructor(string memory name_, string memory symbol_)
    +supportsInterface(bytes4 interfaceId): (bool)
    +balanceOf(address owner): (uint256)
    +ownerOf(uint256 tokenId): (address)
    +name(): (string memory)
    +symbol(): (string memory)
    +tokenURI(uint256 tokenId): (string memory)
    ~_baseURI(): (string memory)
    +approve(address to, uint256 tokenId)
    +getApproved(uint256 tokenId): (address)
    +setApprovalForAll(address operator, bool approved)
    +isApprovedForAll(address owner, address operator): (bool)
    +transferFrom(address from, address to, uint256 tokenId)
    +safeTransferFrom(address from, address to, uint256 tokenId)
    +safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data)
    ~_safeTransfer(address from, address to, uint256 tokenId, bytes memory data)
    ~_ownerOf(uint256 tokenId): (address)
    ~_exists(uint256 tokenId): (bool)
    ~_isApprovedOrOwner(address spender, uint256 tokenId): (bool)
    ~_safeMint(address to, uint256 tokenId)
    ~_safeMint(address to, uint256 tokenId, bytes memory data)
    ~_mint(address to, uint256 tokenId)
    ~_burn(uint256 tokenId)
    ~_transfer(address from, address to, uint256 tokenId)
    ~_approve(address to, uint256 tokenId)
    ~_setApprovalForAll(address owner, address operator, bool approved)
    ~_requireMinted(uint256 tokenId)
    -_checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory data): (bool)
    ~_beforeTokenTransfer(address from, address to, uint256, uint256 batchSize)
    ~_afterTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize)
  }

  ERC721 --|> Context

  %% 608:235:6
  class Context {
    <<Contract>>
    ~_msgSender(): (address)
    ~_msgData(): (bytes calldata)
  }

  ERC721 --|> ERC165

  %% 726:260:9
  class ERC165 {
    <<Contract>>
    +supportsInterface(bytes4 interfaceId): (bool)
  }

  ERC165 --|> IERC165

  %% 405:447:10
  class IERC165 {
    <<Interface>>
    #supportsInterface(bytes4 interfaceId): (bool)$
  }

  ERC721 --|> IERC721

  %% 250:4725:1
  class IERC721 {
    <<Interface>>
    #balanceOf(address owner): (uint256 balance)$
    #ownerOf(uint256 tokenId): (address owner)$
    #safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data)$
    #safeTransferFrom(address from, address to, uint256 tokenId)$
    #transferFrom(address from, address to, uint256 tokenId)$
    #approve(address to, uint256 tokenId)$
    #setApprovalForAll(address operator, bool _approved)$
    #getApproved(uint256 tokenId): (address operator)$
    #isApprovedForAll(address owner, address operator): (bool)$
  }

  IERC721 --|> IERC165

  %% 405:447:10
  class IERC165 {
    <<Interface>>
    #supportsInterface(bytes4 interfaceId): (bool)$
  }

  ERC721 --|> IERC721Metadata

  %% 297:463:4
  class IERC721Metadata {
    <<Interface>>
    #name(): (string memory)$
    #symbol(): (string memory)$
    #tokenURI(uint256 tokenId): (string memory)$
  }

  IERC721Metadata --|> IERC721

  %% 250:4725:1
  class IERC721 {
    <<Interface>>
    #balanceOf(address owner): (uint256 balance)$
    #ownerOf(uint256 tokenId): (address owner)$
    #safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data)$
    #safeTransferFrom(address from, address to, uint256 tokenId)$
    #transferFrom(address from, address to, uint256 tokenId)$
    #approve(address to, uint256 tokenId)$
    #setApprovalForAll(address operator, bool _approved)$
    #getApproved(uint256 tokenId): (address operator)$
    #isApprovedForAll(address owner, address operator): (bool)$
  }

  IERC721 --|> IERC165

  %% 405:447:10
  class IERC165 {
    <<Interface>>
    #supportsInterface(bytes4 interfaceId): (bool)$
  }

Getting started

npm install solidity-mermaid

Getting a Solc Output

In order to get a Solc output, you can use a compilation artifact from your common development enviroment (such as Hardhat or Foundry)

If not, you can always get the output from scratch using solc-js:

import solc from "solc";

const input = {
  language: "Solidity",
  sources: {
    "path/to/your/file.sol": {
      content: `
        // SPDX-License-Identifier: MIT

        ...

        contract Example is ... {
          ...
        }
      `,
    },
  },
  settings: {
    outputSelection: {
      "*": {
        "*": ["*"],
        "": ["ast"],
      },
    },
  },
};

const output = JSON.parse(solc.compile(JSON.stringify(input)));

Solidity AST to Class Diagram

To get a class diagram from your output, you'll need to pass the output, and an AST node with its type and id:

const classDiagram = new Class(output, "ContractDefinition", typeDef.id);

// First run you'll need to use `processed` so the AST gets converted into text
console.log(classDiagram.processed);

// Afterwards, if no changes were made to the AST, you can just print its text
console.log(classDiagram.text);

You can also use it with solidity-ast/utils

import { Class } from "solidity-mermaid";
import { findAll } from "solidity-ast/utils";

for (const [, { ast }] of Object.entries(output.sources)) {
  for (const typeDef of findAll(["ContractDefinition"], ast)) {
    const classDiagram = new Class(output, "ContractDefinition", typeDef.id);

    // ...
  }
}

Solidity Versioning

The Solidity AST should've been produce with a version that's supported in OpenZeppelin's solidity-ast package.

🤝 Contributing

Contributions, issues and feature requests are welcome!
Feel free to check issues page. You can also take a look at the contributing guide.

📝 License

Copyright © 2023 Ernesto García ernestognw@gmail.com.
This project is MIT licensed.