Building a Blended App
Introduction
This guide provides detailed instructions on how to build a blended HelloWorld
application on Fluent. It combines a Rust smart contract to print “Hello” and a Solidity smart contract to print “World.”
This setup demonstrates:
composability between different programming languages (Solidity and Rust)
and interoperability between different virtual machine targets (EVM and Wasm)
within a single execution environment.
Prerequisites
Ensure you have the following installed:
Node.js and npm
Rust and Cargo
Hardhat
pnpm (install via npm:
npm install -g pnpm
)
Step 1: Initialize Your Rust Project
1.1 Set Up the Rust Project
cargo new --lib greeting
cd greeting
1.2 Configure the Rust Project
Cargo.toml
[package]
edition = "2021"
name = "greeting"
version = "0.1.0"
[dependencies]
alloy-sol-types = {version = "0.7.4", default-features = false}
fluentbase-sdk = {git = "https://github.com/fluentlabs-xyz/fluentbase", default-features = false, branch = "dev2"}
[lib]
crate-type = ["cdylib", "staticlib"] #For accessing the C lib
path = "src/lib.rs"
[profile.release]
lto = true
opt-level = 'z'
panic = "abort"
strip = true
[features]
default = []
std = [
"fluentbase-sdk/std",
]
1.3 Write the Rust Smart Contract
src/lib.rs
#![cfg_attr(target_arch = "wasm32", no_std)]
extern crate alloc;
extern crate fluentbase_sdk;
use alloc::string::{String, ToString};
use fluentbase_sdk::{
basic_entrypoint,
derive::{router, signature},
SharedAPI,
};
#[derive(Default)]
struct ROUTER;
pub trait RouterAPI {
fn greeting<SDK: SharedAPI>(&self) -> String;
}
#[router(mode = "solidity")]
impl RouterAPI for ROUTER {
#[signature("function greeting() external returns (string)")]
fn greeting<SDK: SharedAPI>(&self) -> String {
"Hello".to_string()
}
}
impl ROUTER {
fn deploy<SDK: SharedAPI>(&self) {
// any custom deployment logic here
}
}
basic_entrypoint!(ROUTER);
Detailed Code Explanation
1. #![cfg_attr(target_arch = "wasm32", no_std)]
#![cfg_attr(target_arch = "wasm32", no_std)]
This line is a compiler directive. It specifies that if the target architecture is wasm32
(WebAssembly 32-bit), the code should be compiled without the standard library (no_std
). This is necessary for WebAssembly, which doesn't have a full standard library available.
2. extern crate alloc;
and extern crate fluentbase_sdk;
extern crate alloc;
and extern crate fluentbase_sdk;
These lines declare external crates (libraries) that the code depends on.
alloc
is a core library that provides heap allocation functionality.fluentbase_sdk
is the SDK provided by Fluent for writing contracts.
3. use alloc::string::{String, ToString};
use alloc::string::{String, ToString};
This line imports the String
and ToString
types from the alloc
crate. This is necessary because the standard std
library, which normally includes these, is not available in no_std
environments.
4. use fluentbase_sdk::{ basic_entrypoint, derive::{router, signature}, SharedAPI };
use fluentbase_sdk::{ basic_entrypoint, derive::{router, signature}, SharedAPI };
This line imports various items from the fluentbase_sdk
crate:
basic_entrypoint
is a macro for defining the main entry point of the contract.router
andsignature
are macros for routing function calls and defining function signatures.SharedAPI
is a trait that abstracts the API shared between different environments.
5. #[derive(Default)] struct ROUTER;
#[derive(Default)] struct ROUTER;
This line defines a struct named ROUTER
and derives a default implementation for it. The ROUTER
struct will implement the logic for our contract.
6. pub trait RouterAPI { fn greeting<SDK: SharedAPI>(&self) -> String; }
pub trait RouterAPI { fn greeting<SDK: SharedAPI>(&self) -> String; }
This defines a trait named RouterAPI
with a single method greeting
. This method is generic over any type that implements the SharedAPI
trait and returns a String
.
7. #[router(mode = "solidity")] impl RouterAPI for ROUTER { ... }
#[router(mode = "solidity")] impl RouterAPI for ROUTER { ... }
This block implements the RouterAPI
trait for the ROUTER
struct. The #[router(mode = "solidity")]
attribute indicates that this implementation is for a Solidity-compatible router.
Inside the Implementation:
#[signature("function greeting() external returns (string)")]
specifies the function signature in Solidity syntax. This tells the router how to call this function from Solidity.fn greeting<SDK: SharedAPI>(&self) -> String { "Hello".to_string() }
is the implementation of thegreeting
method, which simply returns the string "Hello".
8. impl ROUTER { fn deploy<SDK: SharedAPI>(&self) { // any custom deployment logic here } }
impl ROUTER { fn deploy<SDK: SharedAPI>(&self) { // any custom deployment logic here } }
This block provides an additional method deploy
for the ROUTER
struct. This method can include custom deployment logic. Currently, it's an empty placeholder.
9. basic_entrypoint!(ROUTER);
basic_entrypoint!(ROUTER);
This macro invocation sets up the ROUTER
struct as the main entry point for the contract. It handles necessary boilerplate code for contract initialization and invocation.
Summary
This Rust code defines a smart contract that will be compiled to WebAssembly. The contract implements a single function greeting
that returns the string "Hello". The contract is designed to be called from a Solidity environment, showcasing interoperability between different virtual machines. The basic_entrypoint!
macro ties everything together, making ROUTER
the entry point for the contract.
1.4 Create a Makefile
Makefile
.DEFAULT_GOAL := all
# Compilation flags
RUSTFLAGS := '-C link-arg=-zstack-size=131072 -C target-feature=+bulk-memory -C opt-level=z -C strip=symbols'
# Paths to the target WASM file and output directory
WASM_TARGET := ./target/wasm32-unknown-unknown/release/greeting.wasm
WASM_OUTPUT_DIR := bin
WASM_OUTPUT_FILE := $(WASM_OUTPUT_DIR)/greeting.wasm
# Commands
CARGO_BUILD := cargo build --release --target=wasm32-unknown-unknown --no-default-features
RM := rm -rf
MKDIR := mkdir -p
CP := cp
# Targets
all: build
build: prepare_output_dir
@echo "Building the project..."
RUSTFLAGS=$(RUSTFLAGS) $(CARGO_BUILD)
@echo "Copying the wasm file to the output directory..."
$(CP) $(WASM_TARGET) $(WASM_OUTPUT_FILE)
prepare_output_dir:
@echo "Preparing the output directory..."
$(RM) $(WASM_OUTPUT_DIR)
$(MKDIR) $(WASM_OUTPUT_DIR)
.PHONY: all build prepare_output_dir
1.5 Build the Wasm Project
Run:
make
Step 2: Initialize Your Solidity Project
2.1 Create Your Project Directory
mkdir typescript-wasm-project
cd typescript-wasm-project
npm init -y
2.2 Install Dependencies
npm install --save-dev typescript ts-node hardhat hardhat-deploy ethers dotenv @nomicfoundation/hardhat-toolbox @typechain/ethers-v6 @typechain/hardhat @types/node
pnpm install
npx hardhat
# Follow the prompts to create a basic Hardhat project.
2.3 Configure TypeScript and Hardhat
2.3.1 Update Hardhat Configuration
hardhat.config.ts
import { HardhatUserConfig } from "hardhat/types";
import "hardhat-deploy";
import "@nomicfoundation/hardhat-toolbox";
import "./tasks/greeting"
require("dotenv").config();
const DEPLOYER_PRIVATE_KEY = process.env.DEPLOYER_PRIVATE_KEY || "";
const config: HardhatUserConfig = {
defaultNetwork: "localhost",
networks: {
localhost: {
url: "https://rpc.dev.thefluent.xyz/",
accounts: [DEPLOYER_PRIVATE_KEY],
chainId : 20993,
},
dev: {
url: "https://rpc.dev.thefluent.xyz/",
accounts: [DEPLOYER_PRIVATE_KEY],
chainId : 20993,
},
},
solidity: {
version: "0.8.24",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
namedAccounts: {
deployer: {
default: 0,
},
},
};
export default config;
2.3.2 Update package.json
package.json
package.json
{
"name": "blendedapp",
"version": "1.0.0",
"description": "Blended Hello, World",
"main": "index.js",
"scripts": {
"compile": "npx hardhat compile",
"deploy": "npx hardhat deploy"
}
,
"devDependencies": {
"@nomicfoundation/hardhat-ethers": "^3.0.0",
"@nomicfoundation/hardhat-toolbox": "^5.0.0",
"@nomicfoundation/hardhat-verify": "^2.0.0",
"@openzeppelin/contracts": "^5.0.2",
"@typechain/ethers-v6": "^0.5.0",
"@typechain/hardhat": "^9.0.0",
"@types/node": "^20.12.12",
"dotenv": "^16.4.5",
"hardhat": "^2.22.4",
"hardhat-deploy": "^0.12.4",
"ts-node": "^10.9.2",
"typescript": "^5.4.5"
},
"dependencies": {
"ethers": "^6.12.2",
"fs": "^0.0.1-security"
}
}
2.4 Set Up Environment Variables
Create a .env
file:
DEPLOYER_PRIVATE_KEY=your-private-key-here
Replace
your-private-key-here
with your actual private key.
2.5 Write the Solidity Contracts
In this section, we'll create two Solidity smart contracts: and GreetingWithWorld
.
The interface contract allows the Solidity contract to call the Rust function, demonstrating interoperability between Solidity and Rust within a single execution environment.
The final GreetingWithWorld
contract provides a composable solution that combines the outputs of both the Rust and Solidity contracts.
Create a
contracts
directory and add the following:
2.5.1 Define the Interface
contracts/IFluentGreeting.sol
solidityCopy code// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IFluentGreeting {
function greeting() external view returns (string memory);
}
Detailed Code Explanation
Interface Definition:
The IFluentGreeting
interface declares a single function greeting()
that is external and viewable, meaning it does not modify the state of the blockchain and returns a string. This function will be implemented by another contract and is used to interact with the Rust smart contract.
Interaction with Rust Code:
The greeting
function defined in this interface matches the Rust function that returns a greeting message. The Solidity interface allows the Solidity contract to call the Rust smart contract's function.
2.5.2 Implement the Greeting Contract
contracts/GreetingWithWorld.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./IFluentGreeting.sol";
contract GreetingWithWorld {
IFluentGreeting public fluentGreetingContract;
constructor(address _fluentGreetingContractAddress) {
fluentGreetingContract = IFluentGreeting(_fluentGreetingContractAddress);
}
function getGreeting() external view returns (string memory) {
string memory greeting = fluentGreetingContract.greeting();
return string(abi.encodePacked(greeting, ", World"));
}
}
Detailed Code Explanation
Import Statement: Imports the IFluentGreeting
interface defined earlier.
Contract Definition: Defines a contract GreetingWithWorld
.
State Variable: Declares a state variable fluentGreetingContract
of type IFluentGreeting
. This variable will hold the address of the deployed Rust smart contract.
Constructor:
Takes an address
_fluentGreetingContractAddress
as a parameter.Initializes the
fluentGreetingContract
with the provided address.
Function
getGreeting
:Calls the
greeting
function of thefluentGreetingContract
to get the greeting message from the Rust contract.Concatenates the greeting message with ", World" using
abi.encodePacked
and returns the resulting string.
Interaction with Rust Code:
The
GreetingWithWorld
contract interacts with the Rust smart contract by calling thegreeting
function via theIFluentGreeting
interface.When
getGreeting
is called, it fetches the greeting message ("Hello") from the Rust contract, concatenates it with ", World", and returns the complete greeting ("Hello, World").
How Solidity and Rust Interact:
Rust Smart Contract Deployment: The Rust smart contract is compiled to Wasm and deployed to the blockchain. It contains a function that returns the greeting "Hello".
Solidity Interface (
IFluentGreeting
): The Solidity interface declares agreeting
function that matches the function in the Rust contract.Solidity Implementation (
GreetingWithWorld
):The
GreetingWithWorld
contract uses theIFluentGreeting
interface to interact with the Rust contract.It initializes with the address of the deployed Rust contract.
It calls the
greeting
function of the Rust contract to fetch the greeting message.It concatenates the Rust greeting with ", World" and returns the result.
Step 3: Deploy Both Contracts Using Hardhat
3.1 Create the Deployment Script
This deployment script is responsible for deploying both the Rust smart contract (compiled to Wasm) and the Solidity smart contract (GreetingWithWorld
).
deploy/01_deploy_contracts.ts
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
import { ethers } from "ethers";
import fs from "fs";
import crypto from "crypto";
import path from "path";
require("dotenv").config();
const DEPLOYER_PRIVATE_KEY = process.env.DEPLOYER_PRIVATE_KEY || "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const { deployments, getNamedAccounts, ethers, config, network } = hre;
const { deploy, save, getOrNull } = deployments;
const { deployer: deployerAddress } = await getNamedAccounts();
console.log("deployerAddress", deployerAddress);
// Deploy WASM Contract
console.log("Deploying WASM contract...");
const wasmBinaryPath = "./greeting/bin/greeting.wasm";
const provider = new ethers.JsonRpcProvider(network.config.url);
const deployer = new ethers.Wallet(DEPLOYER_PRIVATE_KEY, provider);
const checkmateValidatorAddress = await deployWasmContract(wasmBinaryPath, deployer, provider, getOrNull, save);
//Deploy Solidity Contract
console.log("Deploying GreetingWithWorld contract...");
const fluentGreetingContractAddress = checkmateValidatorAddress;
const greetingWithWorld = await deploy("GreetingWithWorld", {
from: deployerAddress,
args: [fluentGreetingContractAddress],
log: true,
});
console.log(`GreetingWithWorld contract deployed at: ${greetingWithWorld.address}`);
};
async function deployWasmContract(
wasmBinaryPath: string,
deployer: ethers.Wallet,
provider: ethers.JsonRpcProvider,
getOrNull: any,
save: any
) {
const wasmBinary = fs.readFileSync(wasmBinaryPath);
const wasmBinaryHash = crypto.createHash("sha256").update(wasmBinary).digest("hex");
const artifactName = path.basename(wasmBinaryPath, ".wasm");
const existingDeployment = await getOrNull(artifactName);
if (existingDeployment && existingDeployment.metadata === wasmBinaryHash) {
console.log(`WASM contract bytecode has not changed. Skipping deployment.`);
console.log(`Existing contract address: ${existingDeployment.address}`);
return existingDeployment.address;
}
const gasPrice = (await provider.getFeeData()).gasPrice;
const transaction = {
data: "0x" + wasmBinary.toString("hex"),
gasLimit: 300_000_000,
gasPrice: gasPrice,
};
const tx = await deployer.sendTransaction(transaction);
const receipt = await tx.wait();
if (receipt && receipt.contractAddress) {
console.log(`WASM contract deployed at: ${receipt.contractAddress}`);
const artifact = {
abi: [],
bytecode: "0x" + wasmBinary.toString("hex"),
deployedBytecode: "0x" + wasmBinary.toString("hex"),
metadata: wasmBinaryHash,
};
const deploymentData = {
address: receipt.contractAddress,
...artifact,
};
await save(artifactName, deploymentData);
} else {
throw new Error("Failed to deploy WASM contract");
}
return receipt.contractAddress;
}
export default func;
func.tags = ["all"];
3.2 Create the Hardhat Task
tasks/get-greeting.ts
import { task } from "hardhat/config";
task("get-greeting", "Fetches the greeting from the deployed GreetingWithWorld contract")
.addParam("contract", "The address of the deployed GreetingWithWorld contract")
.setAction(async ({ contract }, hre) => {
const { ethers } = hre;
const GreetingWithWorld = await ethers.getContractAt("GreetingWithWorld", contract);
const greeting = await GreetingWithWorld.getGreeting();
console.log("Greeting:", greeting);
});
3.3 Compile and Deploy the Contracts
Run the following commands to compile and deploy your contracts:
pnpm hardhat compile
pnpm hardhat deploy
pnpm hardhat get-greeting --contract "Deployed Greeting With World Contract Address"
Last updated