# Develop a Contract with Frontend Templates This guide picks up where [Getting Started tutorial](https://developers.stellar.org/docs/build/smart-contracts/getting-started.md) left off. From there, we'll build our own simple frontend template. Building our own template will be a great way to learn how they work. They're not that complicated! ## Make your own template Let’s make our own template! In this example template, we use SolidJS as the JavaScript framework, but other frameworks can be used with minor modifications. The template is using the `hello-world` example smart contract. As a part of the template initialization, bindings for the `hello-world` smart contract are created. This example template is very simple, most of the work goes into creating the `initialize.js` file, which is used to take care of creating a user account, building and deploying the smart contract, and creating the smart contract TypeScript bindings. ### 1. Initialize a SolidJS project ```bash # See https://github.com/solidjs/templates for SolidJS template options npx degit solidjs/templates/vanilla/bare soroban-template-solid cd soroban-template-solid npm install npm run dev ``` The basic SolidJS application is now running on localhost port 3000. #### Dependencies Most of the needed dependencies are already included by the SolidJS template, we just need to add three more: ```bash npm install dotenv glob util ``` The `dotenv` package is needed for reading the environment variables, `glob` is used to find files in the project based on a pattern, and `util` contains a function that can be used to execute system commands asynchronously. #### Smart contract If you already have the `hello-world` smart contract, it can be used for the following steps. If not, run the `stellar contract init heelo-world` CLI command in you project root folder. Since we are going to interact with the smart contract from the `initialize.js` script, add the relative path to the smart contract in the `.env` file in the next step so the `initialize.js` script can find the contract files. The root directory should look like this: ```text ├── hello-world │ └── contracts │ └── hello-world │ ├── src │ │ └── lib.rs │ ├── Cargo.toml │ └── Makefile └── soroban-template-solid ├── node_modules ├── packages ├── src │ ├── App.tsx │ └── index.tsx ├── .env ├── index.html ├── tsconfig.json ├── vite.config.ts ├── initialize.js ├── package.json └── Cargo.toml ``` ### 2. Environment variables The SolidJS code itself doesn’t need environment variables for this simple example, but since we are going to add smart contract bindings, it makes sense to store information about the network and the user in an .env file instead of hard coding those values. These are the variables needed: ```bash PUBLIC_STELLAR_NETWORK="testnet" PUBLIC_STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015" PUBLIC_STELLAR_RPC_URL="https://soroban-testnet.stellar.org" PUBLIC_STELLAR_ACCOUNT="my-user-name" PUBLIC_STELLAR_CONTRACT_PATH="../hello-world" ``` The variables used here are for deploying the contract to testnet and creating the contract bindings for testnet. The user name can be any name, but let’s say you use alice, and have previously created the user `alice` with the Stellar CLI, creating a new account named `alice` will fail. ### 3. initialize.js The goal is to have a script that will handle everything smart contract-related, from creating a user account to deploying the smart contract and providing a TypeScript binding for easy smart contract calls from frontend code. The file `initialize.js` contains that script, and the functionality of it will be broken down in the following sections. #### Definitions Before diving into the functions in the `initialize.js` script, a few constants and variables are defined. The most noteworthy here is `execAsync()`, which will let us execute CLI commands and wait for the command responses. ```javascript // Get directory names const __filename = fileURLToPath(import.meta.url); const dirname = path.dirname(__filename); // Define array to hold deployed smart contract info var smartContracts = Array(); // Run exec commands asynchronously const execAsync = promisify(exec); ``` #### User Now that we have the environment variables, dependencies and definitions taken care of, we can get into the scripts that handle the smart contract deployment and integration. First step towards the integration is to create a user: ```javascript // ###################### Create User ######################## function createUser() { execSync( `stellar keys generate --fund ${process.env.PUBLIC_STELLAR_ACCOUNT} | true`, ); } ``` The user is created by calling the Stellar CLI command `stellar keys generate`, and funding it with Friendbot. The user’s name is fetched from the environment variables. You can check the new user’s public key by running this CLI command: ```bash stellar keys public-key ``` With the public key, you can look up the account on [Stellar Expert](https://stellar.expert/explorer/testnet). #### Build contracts We want the script to build the contract, or contracts in case there are more than one, and it’s a 2-step process. First, we clean up the target folder in case there’s a previous build, and then we call the CLI command to build the contract(s). ```javascript // Remove all previous build files function removeFiles(pattern) { glob(pattern).forEach((entry) => rmSync(entry)); } function buildAll() { removeFiles( `${dirname}/${process.env.PUBLIC_STELLAR_CONTRACT_PATH}/target/wasm32v1-none/release/*.wasm`, ); removeFiles( `${dirname}/${process.env.PUBLIC_STELLAR_CONTRACT_PATH}/target/wasm32v1-none/release/*.d`, ); execSync( `cd ${process.env.PUBLIC_STELLAR_CONTRACT_PATH} && stellar contract build`, ); console.log("Build complete"); } ``` The helper function `removeFiles` will delete any `wasm` or `d` files in the target directory. #### Deploy contracts Now that the smart contract has been built, we can deploy it to the network, so we can invoke the its functions from any client, such as our SolidJS template. There are three functions related to contract deployment. One that uses the Stellar CLI to deploy the Wasm to the network (`deploy()`), one that calls the deploy function for each Wasm found (`deployAll()`), in case there is more than one smart contract, and finally a helper function that gets the contract name by parsing the Wasm file name. ```javascript // Get smart contract name from filename function filenameNoExtension(filename) { return path.basename(filename, path.extname(filename)); } async function deploy(wasm) { // Deploy a single contract and get the contract id const { stdout, stderr } = await execAsync( `stellar contract deploy --wasm ${wasm} --ignore-checks --alias ${filenameNoExtension(wasm)} --source ${process.env.PUBLIC_STELLAR_ACCOUNT} --network ${process.env.PUBLIC_STELLAR_NETWORK} --rpc-url ${process.env.PUBLIC_STELLAR_RPC_URL} --network-passphrase "${process.env.PUBLIC_STELLAR_NETWORK_PASSPHRASE}"`, ); // Add deployed contract to array with alias, wasm path and contract id smartContracts.push({ alias: filenameNoExtension(wasm), wasm: wasm, contractid: stdout.trimEnd(), }); console.log(`Deployed ${filenameNoExtension(wasm)}`); } async function deployAll() { console.log("Deploying all contracts"); const wasmFiles = glob( `${dirname}/${process.env.PUBLIC_STELLAR_CONTRACT_PATH}/target/wasm32v1-none/release/*.wasm`, ); for (const wasm of wasmFiles) { await deploy(wasm); } } ``` The `deploy()` function will get the contract ID from the CLI call, and add the contract name, wasm file path, and contract ID to the `smartContracts[]` array. In this example, we only use one smart contract, but it’s not uncommon to use multiple smart contracts in a dapp, so the template supports the use of multiple contracts. ##### Create bindings The Stellar CLI has a convenient command to create an NPM package that makes it easy to call smart contract functions from a JavaScript/TypeScript-based frontend. We call the package “bindings” because that’s what it does: it binds the contract and the frontend together. As with the contract build functions, the binding function is also capable of handling multiple contracts, so there’s a function for creating the binding package for a contract (`bind()`) and a function that calls `bind()` for each contract (`bindAll()`). ```javascript function bind({ alias, wasm, contractid }) { // Create bindings for a deployed contract execSync( `stellar contract bindings typescript --contract-id ${contractid} --output-dir ${dirname}/packages/${alias} --overwrite`, ); // Build the package execSync(`(cd ${dirname}/packages/${alias} && npm i && npm run build)`); } async function bindAll() { // Bind all deployed contracts for (const contract of smartContracts) { await bind(contract); } } ``` The `bindAll()` function iterates the `smartContracts[]` array. The reason for not just using the array of wasms, like in the `deployAll()` function, is that we need the contract ID to invoke the generated bindings functions on the network. #### Import bindings The last step is to configure the smart contract bindings client. The `importContract()` function creates a TypeScript file with a script that configures a client based on the smart contract ID, the network passphrase, and the RPC URL. The client makes it easy to make calls in the frontend code to the smart contract functions. The file is stored with the contract name as the file name, and with the `.ts` as the extension, e.g., `hello_world.ts`. ```javascript function importContract({ alias, wasm, contractid }) { const outputDir = `${dirname}/src/contracts/`; mkdirSync(outputDir, { recursive: true }); const importContent = `import { Client } from '${alias}';\n` + `export default new Client({\n` + ` contractId: "${contractid}",\n` + ` networkPassphrase: "${process.env.PUBLIC_STELLAR_NETWORK_PASSPHRASE}",\n` + ` rpcUrl: "${process.env.PUBLIC_STELLAR_RPC_URL}",\n` + `${ process.env.PUBLIC_STELLAR_NETWORK === "local" || "standalone" ? ` allowHttp: true,\n` : null }` + `});\n`; const outputPath = `${outputDir}/${alias}.ts`; writeFileSync(outputPath, importContent); console.log(`Created import for ${alias}`); } function importAll() { smartContracts.forEach(importContract); } ``` #### Main function At last, we have the main function, which calls the above functions in the right order. Note the asynchronous calls of `deployAll()` and `bindAll()`. The functions following them depend on the completion of the previous functions. ```javascript // Calling the functions in sequence async function main() { createUser(); buildAll(); await deployAll(); await bindAll(); importAll(); } main().catch((e) => { console.error("Initialization failed", e); process.exit(1); }); ``` #### Complete initialize.js file This is the complete file. Place it in the SolidJS root: ```javascript import "dotenv/config"; import { mkdirSync, writeFileSync, rmSync, readFileSync } from "fs"; import { execSync, exec } from "child_process"; import path from "path"; import { fileURLToPath } from "url"; import { sync as glob } from "glob"; import { promisify } from "util"; // ###################### Definitions ######################## // Get directory names const __filename = fileURLToPath(import.meta.url); const dirname = path.dirname(__filename); // Define array to hold deployed smart contracts var smartContracts = Array(); // Run exec commands asynchronously const execAsync = promisify(exec); // ###################### Create User ######################## function createUser() { execSync( `stellar keys generate --fund ${process.env.PUBLIC_STELLAR_ACCOUNT} | true`, ); } // ###################### Build Contracts ######################## // Remove all previous build files function removeFiles(pattern) { glob(pattern).forEach((entry) => rmSync(entry)); } function buildAll() { removeFiles(`${dirname}/target/wasm32v1-none/release/*.wasm`); removeFiles(`${dirname}/target/wasm32v1-none/release/*.d`); execSync(`stellar contract build`); console.log("Build complete"); } // ###################### Deploy Contracts ######################## // Get smart contract name from filename function filenameNoExtension(filename) { return path.basename(filename, path.extname(filename)); } async function deploy(wasm) { // Deploy a single contract and get the contract id const { stdout, stderr } = await execAsync( `stellar contract deploy --wasm ${wasm} --ignore-checks --alias ${filenameNoExtension(wasm)} --source ${process.env.PUBLIC_STELLAR_ACCOUNT} --network ${process.env.PUBLIC_STELLAR_NETWORK} --rpc-url ${process.env.PUBLIC_STELLAR_RPC_URL} --network-passphrase "${process.env.PUBLIC_STELLAR_NETWORK_PASSPHRASE}"`, ); // Add deployed contract to array with alias, wasm path and contract id smartContracts.push({ alias: filenameNoExtension(wasm), wasm: wasm, contractid: stdout.substring(0, stdout.length - 1), }); console.log(`Deployed ${filenameNoExtension(wasm)}`); } async function deployAll() { console.log("Deploying all contracts"); const wasmFiles = glob(`${dirname}/target/wasm32v1-none/release/*.wasm`); for (const wasm of wasmFiles) { await deploy(wasm); } } // ###################### Create Bindings ######################## function bind({ alias, wasm, contractid }) { // Create bindings for a deployed contract execSync( `stellar contract bindings typescript --contract-id ${contractid} --output-dir ${dirname}/packages/${alias} --overwrite`, ); // Build the package execSync(`(cd ${dirname}/packages/${alias} && npm i && npm run build)`); // Install the package execSync(`pnpm add file:./packages/${alias}`); } async function bindAll() { // Bind all deployed contracts for (const contract of smartContracts) { await bind(contract); } } // ###################### Import Bindings ######################## function importContract({ alias, wasm, contractid }) { const outputDir = `${dirname}/src/contracts/`; mkdirSync(outputDir, { recursive: true }); const importContent = `import * as Client from '${alias}';\n` + `export default new Client.Client({\n` + ` contractId: "${contractid}",\n` + ` networkPassphrase: "${process.env.PUBLIC_STELLAR_NETWORK_PASSPHRASE}",\n` + ` rpcUrl: "${process.env.PUBLIC_STELLAR_RPC_URL}",\n` + `${ process.env.PUBLIC_STELLAR_NETWORK === "local" || "standalone" ? ` allowHttp: true,\n` : null }` + `});\n`; const outputPath = `${outputDir}/${alias}.ts`; writeFileSync(outputPath, importContent); console.log(`Created import for ${alias}`); } function importAll() { smartContracts.forEach(importContract); } // ###################### Main ######################## // Calling the functions in sequence async function main() { createUser(); buildAll(); await deployAll(); await bindAll(); importAll(); } main().catch((e) => { console.error("Initialization failed", e); process.exit(1); }); ``` ### 4. Modify Vite config SolidJS is using the build tool Vite, and we need to make a minor addition to the Vite configuration file (`vite.config.ts`) for the module exports to work. Add these lines to the config: ```javascript optimizeDeps: { include: ['@stellar/stellar-sdk', 'hello_world'], }, ``` ### 5. Build the frontend The template is now ready to use the smart contract and its binding through the client. Let’s build a very simple dapp, a frontend for the Hello World smart contract, with a text input field and a send button. When a user enters a text string in the input field and clicks the send button, the contract function is invoked with the text string as the argument. The returned value, a string array, is displayed in the frontend. Here’s an example of how the code could look the existing code in the `src/App.tsx` file with this code: ```javascript import type { Component } from 'solid-js'; import { createSignal } from "solid-js"; import helloWorld from './contracts/hello_world'; const App: Component = () => { const [input, setInput] = createSignal(''); const [greeting, setGreeting] = createSignal(''); const [loading, setLoading] = createSignal(false); const [error, setError] = createSignal(null); async function getGreeting(e?: Event) { e?.preventDefault(); setError(null); setLoading(true); try { const { result } = await helloWorld.hello({ to: input() || 'you' }); const greet = Array.isArray(result) ? result.join(' ') : String(result); setGreeting(greet); } catch (err: any) { console.error(err); setError(err?.message || 'Unknown error'); } finally { setLoading(false); } } return (

Hello Soroban Solid Template!

setInput(e.target.value)} />
{error() &&

Error: {error()}

}

{greeting()}

); }; export default App; ``` ### 6. Try it out We can now run the code with this command: ```bash npm run dev ``` The URL for the dapp will be shown in the terminal, typically it’s `http://localhost:3000` unless the port 3000 is already in use.