DEV Community

Apperside
Apperside

Posted on

Creating a web3 DApp with a NX Monorepo - 1 of 5: Libraries with solidity contracts

This is the second episode of a series about the work I've done to create a web3 DApp in a NX monorepo.

Check the previous article to have a better context understanding

BALUNI CONTRACTS and BALUNI HYPERVISOR CONTRACTS

I started the migration with these 2 projects because they are at the bottom most of the dependency chain.

Creating the NX package

These 2 projects are not runnable ones, but they are libraries, so I went through the vast collection of templates that NX offers (you can find the full list here).

From that list, the library generator seemed the most reasonable choice, so I ran the command provided in the docs

npx nx g @nx/js:lib baluni-contracts
Enter fullscreen mode Exit fullscreen mode

it inrectactively asked me some questions:

Image description

Image description

Image description

In the last prompt, I chose the second option, and it told me that this option is going to be deprecated in the near future in favor of this syntax:

nx g @nx/js:library baluni-contracts --directory packages/baluni-contracts
Enter fullscreen mode Exit fullscreen mode

This is less dev friendly so I don't understand why they remove this.

After making all the choices, NX quickly generated the project

Image description

It is a bare bone project with a pre-configured entry point (index.ts) that exports from the lib folder

So far, so good.

I wont dig into every single file otherwise it would become an endless article.

When you generate a library with NX, you can’t β€œrun” it, you can just build it or include it in other projects, so I tried to build it to see what the output is

Image description

I didn’t like much that the build contains an src folder but for the moment I ignored it and went forward to integrate the rest of the project.

Importing files

I don't have any knowledge about smart contracts, solidity, hard etc, but I have enough experience to know that a project is made of

  • configuration files (like .eslintrc, .prettierrc, tsconfig.ts etc..),
  • source code
  • assets
  • dependencies.

For The first ones we'll use the ones NX has generated for us, so we need to import all the rest from the old project.

Let's start from the dependencies.

Integrate package.json dependencies

NX created the project with a package.json specific for the library, this makes sense as it comes handy when we compile it to publish on npm.

I always knew that the rule of thumb in NX projects is to add dependencies to the root package.json, but when I analyzed the original projects I saw different versions of the the same libraries (like hardhard, ehers and others) across projects, so I decided to take a conservative approach and added them to baluni-contracts's package.json and instruct yarn to not hoist dependencies for this project, as they would clash later when adding the other projects.

Avoid hoisting

To instruct yarn to not hoist dependencies for this project I found a useful setting on yarn's documentation.
So I had to add this:

baluni-contracts/package.json
  "installConfig": {
    "hoistingLimits": "dependencies"
  },
Enter fullscreen mode Exit fullscreen mode
snippet of baluni-contracts/package.json

Snippet of baluni-contracts/package.json

After running yarn I was sad to see that al node_modules have all been hoisted at the root, except few. Not as expected

Image description

After struggling I bit, I understood that flag is for more recent versions of yarn, and I was using yarn 1.

Image description

So I switched to the last version of yarn:

yarn set version stable
Enter fullscreen mode Exit fullscreen mode

and ran yarn again.

This time, after an apparently successfully operation, the node_modules folder didn't exist at all πŸ€”

Image description

After some more research, I found that it was because recent versions of yarn use, by default, a different mechanism for handling modules, but I don't know how to use it and this is not the moment to learn it (but I would like to learn it, maybe I'll speak about it in the future).

To make things work with node_modules, I had to add this to the file .yarnrc.yml(it was created by yarn when we upgraded it).

The created file was this:

yarnPath: .yarn/releases/yarn-4.3.1.cjs
Enter fullscreen mode Exit fullscreen mode

But I had to add this:

yarnPath: .yarn/releases/yarn-4.3.1.cjs
+ nodeLinker: node-modules
Enter fullscreen mode Exit fullscreen mode

it tells to yarn to use the node_modules way to save modules.

Now after running yarn from the monorepo root I can successfully see all the needed dependencies under packages/baluni-contracts/node_modules

Image description

GREAT πŸ’ͺ

Integrate assets

Now it's time to integrate the source code and the assets (and few configs) in the new codebase.

The original project structure was this:

Image description

At a first sight, it looks we need to bring in all the folders in the project root and some configuration file, so in the end I decided to bring in the root of the new project everything from the original one except:

  • .github folder (I will handle actions in a later iteration)
  • .yarn folder (we have our own)
  • cache
  • typechain-types (they are auto generated from build scripts)
  • all configuration files (eslint, pretties, tsconfig ecc..), except for **.solhint.json** (needed by solidity), **hardhat.config.ts**,

And the final thing was to bring in the package.json's scripts

 "scripts": {
    "account": "hardhat run scripts/listAccount.ts",
    "chain": "hardhat node --network hardhat --no-deploy",
    "compile": "hardhat compile",
    "deploy": "hardhat deploy",
    "fork": "MAINNET_FORKING_ENABLED=true hardhat node --network hardhat --no-deploy",
    "generate": "hardhat run scripts/generateAccount.ts",
    "lint": "eslint --config ./.eslintrc.json --ignore-path ./.eslintignore ./*.ts ./deploy/**/*.ts ./scripts/**/*.ts ./test/**/*.ts",
    "lint-staged": "eslint --config ./.eslintrc.json --ignore-path ./.eslintignore",
    "test": "REPORT_GAS=true hardhat test --network hardhat",
    "verify": "hardhat etherscan-verify",
    "hardhat-verify": "hardhat verify"
  },
Enter fullscreen mode Exit fullscreen mode

At this point, the only thing left to do is to verify if we can compile the contracts:

Image description

cd packages/baluni-contracts
yarn compile
Enter fullscreen mode Exit fullscreen mode

This is the output

➜  ~/dev/temp/baluni-monorepo git:(main) cd packages/baluni-contracts                                                            [9:54:01]
➜  ~/dev/temp/baluni-monorepo/packages/baluni-contracts git:(main) βœ— yarn compile                                                [9:56:08]
Solidity 0.8.25 is not fully supported yet. You can still use Hardhat, but some features, like stack traces, might not work correctly.

Learn more at https://hardhat.org/hardhat-runner/docs/reference/solidity-support

Warning: Unnamed return variable can remain unassigned. Add an explicit return with value to all non-reverting code paths or name the variable.
   --> contracts/pools/BaluniV1Pool.sol:691:75:
    |
691 |     function getAssetReserve(address asset) public view override returns (uint256) {
    |                                                                           ^^^^^^^


Warning: Unused function parameter. Remove or comment out the variable name to silence this warning.
   --> contracts/pools/BaluniV1Pool.sol:870:32:
    |
870 |     function generateTokenName(address[] memory _assets) internal pure returns (string memory) {
    |                                ^^^^^^^^^^^^^^^^^^^^^^^^


Warning: Unused try/catch parameter. Remove or comment out the variable name to silence this warning.
   --> contracts/vaults/BaluniV1YearnVault.sol:257:22:
    |
257 |             } catch (bytes memory reason) {
    |                      ^^^^^^^^^^^^^^^^^^^


Warning: Function state mutability can be restricted to pure
  --> contracts/mock/MockRebalancer.sol:93:5:
   |
93 |     function checkRebalance(
   |     ^ (Relevant source part starts here and spans across multiple lines).


Warning: Function state mutability can be restricted to pure
   --> contracts/mock/MockRebalancer.sol:101:5:
    |
101 |     function getBaluniRouter() external view returns (address) {
    |     ^ (Relevant source part starts here and spans across multiple lines).


Warning: Contract code size is 30854 bytes and exceeds 24576 bytes (a limit introduced in Spurious Dragon). This contract may not be deployable on Mainnet. Consider enabling the optimizer (with a low "runs" value!), turning off revert strings, or using libraries.
  --> contracts/orchestators/BaluniV1Router.sol:58:1:
   |
58 | contract BaluniV1Router is
   | ^ (Relevant source part starts here and spans across multiple lines).


Warning: Contract code size is 33507 bytes and exceeds 24576 bytes (a limit introduced in Spurious Dragon). This contract may not be deployable on Mainnet. Consider enabling the optimizer (with a low "runs" value!), turning off revert strings, or using libraries.
  --> contracts/pools/BaluniV1Pool.sol:54:1:
   |
54 | contract BaluniV1Pool is
   | ^ (Relevant source part starts here and spans across multiple lines).


Generating typings for: 76 artifacts in dir: typechain-types for target: ethers-v6
Successfully generated 224 typings!
Compiled 71 Solidity files successfully (evm targets: cancun, istanbul).
➜  ~/dev/temp/baluni-monorepo/packages/baluni-contracts git:(main) βœ—       
Enter fullscreen mode Exit fullscreen mode

BAM!πŸŽ‰
Even with some warning (that was present also in the original project) it worked, so all the tools and dependencies are working.

Making it publishable

The final thing was to make it publishable, and it means that it need to have the source and built contracts in the published package.

If we try do build the project now, the build folder will be like this:

Image description

but that's not what we want, we also need the contracts and artifacts folders.

Let's configure the project for that:

we need just to tell nx to include those assets in the build target configuration, so we need to modify packages/baluni-contracts/project.json.
The file content generated by NX was this

{
  "name": "baluni-contracts",
  "$schema": "../../node_modules/nx/schemas/project-schema.json",
  "sourceRoot": "packages/baluni-contracts/src",
  "projectType": "library",
  "tags": [],
  "targets": {
    "build": {
      "executor": "@nx/js:tsc",
      "outputs": ["{options.outputPath}"],
      "options": {
        "outputPath": "dist/packages/baluni-contracts",
        "main": "packages/baluni-contracts/src/index.ts",
        "tsConfig": "packages/baluni-contracts/tsconfig.lib.json",
        "assets": ["packages/baluni-contracts/*.md"]
      }
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

for our purposes we need to edit the assets to include what we need

Image description

Let's try to build the project now to see what the output looks like:

Image description

Amazing!

Image description

You will notice that we have that src folder that does actually nothing because the library just exposes contracts and to not contains code, we leave this folder there for now.
Apart from this, in our build folder we have a package.json and our assets, this build output seems perfect to be published to npm as a standalone package.

Job 1 Done! πŸŽ‰

An identical approach has been used to bring in the other project containing contracts (baluni-hypervisor-contracts).

In the following articles we'll see the integration of baluni-core that will consume the 2 library project we just created

Top comments (0)