Develop contract initialization templates
This guide will walk you through the process of developing contract templates for working with Stellar smart contracts in frontend applications. This guide will cover the process of creating or developing the templates itself as well as how to work with the template to build, deploy, and generate smart contract bindings for the template.
Prerequisites
Before you begin, ensure you have the following installed:
- Rust and Cargo (for compiling smart contracts)
- Node.js and npm (for running JavaScript deployment scripts)
- Stellar CLI
- React
Anatomy of contract initialization (init) template
Stellar smart contracts are written in Rust, but suppose we want our frontend web application to work with these contracts: this is where templates come into the equation. Templates allow us to work with smart contracts in our frontend application. At its core a template consists of a frontend library and a script for interacting with the Stellar CLI.
Keep in mind you can use any frontend library to work with Stellar smart contracts. In this guide we'll use React to generate the template but it is worth noting that you can use whichever library you like or no library at all.
We'll start off by installing the React library and then we'll walk through creating the initialization script, and finally wrap up by using the template to build, deploy, and generate contract bindings.
Creating the frontend
The following command initializes a new Vite React project and installs the necessary dependencies:
npm create vite@latest soroban-react-template --template react
cd soroban-react-template
npm install
npm run dev
Creating the initialize script
The initialization script is the heart of the contract template. This little but highly crucial script enables us to execute Stellar CLI commands which, in turn, enables us to build, deploy, and generate smart contract bindings.
The initialization script can be written in bash script or JavaScript. This guide uses the latter but the same principles are involved when using bash scripts.
We start off by creating initialize.js
files in our frontend project directory.
touch initialize.js
This command will create the initailze.js
file in your project's directory. Now let's open the file to begin writing the necessary code
Installing the dependencies
The following dependencies will simplify the process of interacting with files and directories which the initialize.js
file does together with other functionalities.
Install the above packages as dev dependencies by running the command below:
- npm
- Yarn
- pnpm
npm install dotenv glob --dev
yarn add dotenv glob --dev
pnpm add dotenv glob --dev
Writing the initialization code
As a general overview, the following script will perform the below tasks:
- Create network profiles for contract project
- Generate and fund account for interaction with the Stellar network
- Build smart contract .wasm files
- Deploy smart contract to the Stellar network
- Generate smart contract Typescript bindings
Importing the modules
The following modules allow us to work with environment variables, execute Stellar CLI commands, search files and directories, work with paths, and create and delete files, in that order.
import "dotenv/config";
import { execSync } from "child_process";
import { globSync } from "glob";
import path from "path";
import { writeFileSync, rmSync } from "fs";
Creating the wrapper function
Instead of calling the execSync
function and making the error handling process multiple times, we create a wrapper function to encapsulate this function.
Furthermore, when deploying the contract, the command returns the deployed contract address, which is required when generating contract Typescript bindings. Wrapping the execSync
function will enable the script to return Stellar CLI command output while handling errors in one go.
const run = (command) => {
try {
const output = execSync(command, { stdio: ["inherit", "pipe", "pipe"] });
console.log("Here comes the output\n");
return output.toString();
} catch (error) {
console.error("Error executing command: \n", error.message);
return null; // or throw error if you want to propagate it
}
};
Creating network profiles
This code snippet executes a Stellar CLI command to add a network profile. It uses environment variables (process.env.NETWORK, process.env.RPC_URL, process.env.NETWORK_PASSPHRASE) to specify the network details and RPC URL.
You can clearly see the benefit of wrapping the execSync
function as it results in a much cleaner code and short snippet while retaining same functionality.
run(
`stellar network add ${process.env.NETWORK} --rpc-url ${process.env.RPC_URL} --network-passphrase ${process.env.NETWORK_PASSPHRASE}`,
);
Creating account profiles
This code generates Stellar key pairs for an account (process.env.ACCOUNT) and funds them accordingly on a specific network (process.env.NETWORK).
run(
`stellar keys generate ${process.env.ACCOUNT} --network ${process.env.NETWORK}`,
);
Building the contract
This snippet builds Stellar smart contracts in the project directory.
run("stellar contract build");
Deploying the contract
This code deploys one or more Stellar smart contracts. It uses files found in target/wasm32-unknown-unknown/release/\*.wasm
, deploys each with specified parameters, and stores contract details (alias and contractId) in contractsObject.
The code iterates through the smart contract files found in the project directory and deploys all of them.
const files = globSync("target/wasm32-unknown-unknown/release/*.wasm");
let alias, contractId;
console.log(files);
files.forEach((file) => {
alias = path.basename(file).split(".")[0];
console.log(`Deploying Contract ${alias}`);
contractId = run(
`stellar contract deploy --network ${process.env.NETWORK} --source ${process.env.ACCOUNT} --wasm ${file} --alias ${alias}`,
);
//save contractId and alias to contracts Object
contractsObject[alias] = {
alias,
id: contractId,
};
});
Generating TypeScript bindings
This snippet generates TypeScript bindings for Stellar smart contracts deployed earlier. It iterates over files and uses contractsObject
to obtain contract details (alias
and contractId
).
let filename, output_dir;
files.forEach((file) => {
filename = path.basename(file).split(".")[0];
output_dir = `./packages/${filename}`;
run(
`stellar contract bindings typescript --output-dir ${output_dir} --network ${process.env.NETWORK} --contract-id ${contractsObject[alias]["id"]} --wasm ${file} --overwrite`,
);
});
Complete initialization script
The following snippet is the complete aggregated version of the snippets shown earlier. I've added some functionalities like deleting pre-existing contract Wasm files and creating a contract.json
file, which are all optional steps.
import "dotenv/config";
import { execSync } from "child_process";
import { globSync } from "glob";
import path from "path";
import { writeFileSync, rmSync } from "fs";
//Create An object to hold the Contract IDs and Alias
const contractsObject = {};
//Create a wrapper around execSync function to handle errors and makes its return easily available withing the script
const run = (command) => {
try {
const output = execSync(command, { stdio: ["inherit", "pipe", "pipe"] });
console.log("Here comes the output\n");
return output.toString();
} catch (error) {
console.error("Error executing command: \n", error.message);
return null; // or throw error if you want to propagate it
}
};
//Configure the network for the project
console.log("Configuring Network");
run(
`stellar network add ${process.env.NETWORK} --rpc-url ${process.env.RPC_URL} --network-passphrase ${process.env.NETWORK_PASSPHRASE}`,
);
//Generate and fund the Wallet Profile for the project
run(
`stellar keys generate ${process.env.ACCOUNT} --network ${process.env.NETWORK}`,
);
//Delete Remove all pre-existing Wasm Files
const existingFiles = globSync("./target/**.wasm");
console.log(`Existing Files ${existingFiles}`);
existingFiles.forEach((filePath, index) => {
try {
console.log(
`Deleting ${path.basename(filePath)} ${index + 1} out of ${
existingFiles.length
}`,
);
rmSync(filePath);
} catch (err) {
console.error(`Error while deleting Files \n ${err}`);
}
});
//Build Wasm Contract Files
run("stellar contract build");
//Deploy Contract Wasm and save the contact Ids and Aliases
const files = globSync("target/wasm32-unknown-unknown/release/*.wasm");
let alias, contractId;
console.log(files);
files.forEach((file) => {
alias = path.basename(file).split(".")[0];
console.log(`Deploying Contract ${alias}`);
contractId = run(
`stellar contract deploy --network ${process.env.NETWORK} --source ${process.env.ACCOUNT} --wasm ${file} --alias ${alias}`,
);
//save contractId and alias to contracts Object
contractsObject[alias] = {
alias,
id: contractId,
};
});
//Create a json File to store the contract name as key and contract id and contract alias contained in an object as value
const contractsJSONPath = "./contracts.json";
const contractsJSONData = JSON.stringify(contractsObject);
try {
writeFileSync(contractsJSONPath, contractsJSONData);
console.log(`JSON file has been saved to ${contractsJSONPath}`);
} catch (err) {
console.error("Error writing JSON file: ", err);
}
//Generate Contract Typescript Bindings
let filename, output_dir;
files.forEach((file) => {
filename = path.basename(file).split(".")[0];
output_dir = `./packages/${filename}`;
run(
`stellar contract bindings typescript --output-dir ${output_dir} --network ${process.env.NETWORK} --contract-id ${contractsObject[alias]["id"]} --wasm ${file} --overwrite`,
);
});
Modifying package.json
file
After saving the initialize.js
script, let's modify our package.json
file to create a new entry under scripts to run the code to work on the contract files.
Let's also modify the file to create a new workspace in the package.json
to point to the path where the the TypeScript bindings are generated
"scripts": {
"setup": "node initialize.js",
},
"workspace": ["./packages/*"],
Running the script
Now that we have a complete script and a frontend, we should try to run the script. However, because the script is dependent on contract files we need to have a contract project ready, otherwise the script will fail and result in an error. To solve this issue we have to either copy the whole project contents and paste them to an existing Stellar contract project directory or upload the template file to a repo then create a new project and setting the 'f' parameter to point to the hosted repository of the template.
Setting up the project
A community member has hosted a public repository of the React template available for use. The following command implements the second option:
stellar contract init hello_contract -f https://github.com/CaptainPrinz/soroban-react-template
The snippet above initializes a new Stellar smart contracts project, hello_contract
, that uses a prebuilt hello_world
contract and adds the frontend template we deployed to a remote repository.
Setting the environment variables
Our templates offer a great deal of customization via environment variables. These variables need to be set by the user by creating a .env
file otherwise the process will fail as these variables will be undefined.
Starting the script
On your terminal run the following code:
npm install
cp .env.example .env # edit .env file as needed
npm run setup
This runs the initialize.js
script and first sets the network profile, account identities, builds the contract .wasm
file, deploys the contracts, and generates the bindings.
If successful, there should be the following changes in the directory:
- A new
.soroban
directory, which contains the network profile files, account identity files, and hosted smart contract files in json format - A
packages
directory containing the generated TypeScript bindings - A
contracts.json
file
You are now finally able to import these generated bindings in your frontend application, create their clients, and use them to interact with the contracts deployed on the network.
Interacting with the contract bindings
After running the initialization script, there should be new packages
directory which confirms successful bindings generation. Navigate to the packages
directory and there should be a package for the deployed contract(s). This package will form the last piece in solving the puzzle of interacting with soroban contracts from frontend appications which will be discussed briefly.
To work with the bindings generated, it is recommended the wrap the bindings by creating a new module, instantiating a new client, then finally importing the client and invoking the contract functions.
While it is not necessary to follow the procedure above, you stand to have a better code structure and a much easier way of fixing the code should anything go wrong.
However given the simple structure of our contract, we will proceed to integrate it directly in the frontend without creating a wrapper module.
Setting up the hello world package
Navigate to the packages folder to find the hello world package and observe the directory contents. If the directory does not contain a dist
and node modules
folder, you must install the dependencies and build the hello world typescript bindings for the package to work in our react app.
To install the dependencies and build the typescript bindings for the package, navigate to the packages
directory, then enter hello_world
package directory and run the command below.
npm i
npm run build
After running the command above, there should now be a node modules
and a dist
directory containing index.js
and index.ts
files. These will serve as an entry point to the package when it is eventually imported.
Using the hello world package in React
First we import the following from the package:
- A
networks
object. - A
Client
class.
import { Client, networks } from "../packages/hello_world";
Notice how we point to the directory of the package when importing th ebindings by making reference to the packages folder.
Now we instantiate a the Client
class and pass an object containing the RPC url and a copy of the networks.testnet
object by using the spread operator. This technically means that the Client
class takes in an object of the network configuration along with the contract ID but we make it easier by using the networks
object.
const helloWorldContract = new Client({
...networks.testnet,
rpcUrl: "https://soroban-testnet.stellar.org:443",
});
By printing the helloWorldContract
object, you'll find the hello method which coincides with the hello function defined in the smart contract. Calling this method will invoke its equivalent in the smart contract.
Finally, to interact with the contract we run the following code:
const { result } = await contract.hello({ to: receiver });
Modifying the App component
To put it all together, modify the App.jsx
component in the src
directory by replacing its content with the code below.
import { Client, networks } from "../packages/hello_world";
import { useState } from "react";
function App() {
const contract = new Client({
...networks.testnet,
rpcUrl: "https://soroban-testnet.stellar.org:443",
});
console.log(contract);
const [target, setTarget] = useState("");
let receiver;
const getResult = async () => {
const { result } = await contract.hello({ to: receiver });
console.log(result);
setTarget(result);
};
const handleSubmit = (e) => {
e.preventDefault();
getResult();
};
return (
<>
<h1>Response: {target}</h1>
<form onSubmit={handleSubmit}>
<input type="text" onChange={(e) => (receiver = e.target.value)} />
<input type="submit" value="Say hello" />
</form>
</>
);
}
export default App;
Now run npm run dev
to run the project and play with the deployed smart contract from your frontend app.
Customization
With the initialize.js
script being the heart of the template file, you can go through the file and modify the paths in which files are saved if necessary.
Also a great deal of customization is enabled by using environment variables, giving flexibility to change network profiles and account identity without limits.
Conclusion
A lot has been covered from creating template files to building, deploying, generating bindings, and interacting with smart contracts. Now go ahead and play with the code and try applying the procedures to other contracts.
Guides in this category:
📄️ Use Docker to build and run dapps
Understand Docker and use it to build applications
📄️ Comprehensive frontend guide for Stellar dapps
Learn how to build functional frontend interfaces for Stellar dapps using React, Tailwind CSS, and the Stellar SDK.
📄️ Initialize a dapp using scripts
Set up initialization correctly to ensure seamless setup for your dapp
📄️ Create a frontend for your dapp using React
Connect dapp frontends to contracts and Freighter wallet using @soroban-react
📄️ Develop contract initialization templates
Create frontend templates that can be used to work with Stellar smart contracts in frontend applications
📄️ Implement state archival in dapps
Learn how to implement state archival in your dapp
📄️ Work with contract specs in Java, Python, and PHP
A guide to understanding and interacting with Soroban smart contracts in different programming languages