Skip to main content

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:

  1. Rust and Cargo (for compiling smart contracts)
  2. Node.js and npm (for running JavaScript deployment scripts)
  3. Stellar CLI
  4. 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.

  1. glob
  2. dotenv

Install the above packages as dev dependencies by running the command below:

npm install dotenv glob --dev

Writing the initialization code

As a general overview, the following script will perform the below tasks:

  1. Create network profiles for contract project
  2. Generate and fund account for interaction with the Stellar network
  3. Build smart contract .wasm files
  4. Deploy smart contract to the Stellar network
  5. 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:

  1. A new .soroban directory, which contains the network profile files, account identity files, and hosted smart contract files in json format
  2. A packages directory containing the generated TypeScript bindings
  3. 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:

  1. A networks object.
  2. 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.