diff --git a/docs/build/apps/dapp-frontend.mdx b/docs/build/apps/dapp-frontend.mdx index bc11c378a..a58e33c5a 100644 --- a/docs/build/apps/dapp-frontend.mdx +++ b/docs/build/apps/dapp-frontend.mdx @@ -1,561 +1,536 @@ --- sidebar_position: 70 -sidebar_label: Build a Dapp Frontend +sidebar_label: Develop a Contract with Frontend Templates title: "Build a dapp Frontend: Connect Wallets, Handle Transactions & More" description: "Learn how to build a dapp frontend that connects to smart contracts. Explore best practices for integrating wallets, handling transactions, and interacting with the Stellar network." --- -# Build a Dapp Frontend +# Develop a Contract with Frontend Templates -This is a continuation of the [Getting Started tutorial](../smart-contracts/getting-started/README.mdx), where you should have deployed two smart contracts to the public network. In this section, we'll create a web app that interacts with the contracts via RPC calls. +This guide picks up where [Build a Dapp Frontend](https://developers.stellar.org/docs/build/apps/dapp-frontend) left off. From there, we'll: -Let's get started. +1. Search GitHub for other Soroban templates +2. Build our own simple template -## Initialize a frontend toolchain +Building our own template will be a great way to learn how they work. They're not that complicated! -You can build a Soroban app with any frontend toolchain or integrate it into any existing full-stack app. For this tutorial, we're going to use [Astro](https://astro.build/). Astro works with React, Vue, Svelte, any other UI library, or no UI library at all. In this tutorial, we're not using a UI library. The Soroban-specific parts of this tutorial will be similar no matter what frontend toolchain you use. +## Search GitHub for other Soroban templates -If you're new to frontend, don't worry. We won't go too deep. But it will be useful for you to see and experience the frontend development process used by Soroban apps. We'll cover the relevant bits of JavaScript and Astro, but teaching all of frontend development and Astro is beyond the scope of this tutorial. +The official template maintained by Stellar Development Foundation (SDF), as used in [Build a Dapp Frontend](https://developers.stellar.org/docs/build/apps/dapp-frontend), lives on GitHub at [stellar/soroban-template-astro](https://github.com/stellar/soroban-astro-template). It uses the [Astro web framework](https://astro.build/). While Astro works with React, Vue, Svelte, and any other UI library, the template opts not to use them, preferring Astro's own templating language, which uses vanilla JavaScript with no UI library. -Let's get started. +(You may wonder why it makes this unpopular choice. A fair question! The team wanted to balance actual utility with broad approachability. Not everyone learning Stellar and Soroban is familiar with React, or any other UI library. It also demonstrates that core Soroban libraries all work with any JavaScript project.) -You're going to need [Node.js](https://nodejs.org/en/download/package-manager/) v18.14.1 or greater. If you haven't yet, install it now. - -We want to create an Astro project with the contracts from the previous lesson. To do this, we can clone a template. You can find Soroban templates on GitHub by [searching for repositories that start with "soroban-template-"](https://github.com/search?q=%22soroban-template-%22&type=repositories). For this tutorial, we'll use [stellar/soroban-template-astro](https://github.com/stellar/soroban-template-astro). We'll also use a tool called [degit](https://github.com/Rich-Harris/degit) to clone the template without its git history. This will allow us to set it up as our own git project. - -Since you have `node` and its package manager `npm` installed, you also have `npx`. - -We're going to create a new project directory with this template to make things easier in this tutorial, so make sure you're no longer in your `soroban-hello-world` directory and then run: - -```sh -npx degit stellar/soroban-template-astro first-soroban-app -cd first-soroban-app -git init -git add . -git commit -m "first commit: initialize from stellar/soroban-template-astro" -``` - -This project has the following directory structure, which we'll go over in more detail below. +To use other templates, we will clone them from their repositories, and then copy these files into the root of the existing `soroban-hello-world directory`: ```bash -├── contracts -│   ├── hello_world -│   └── increment -├── CONTRIBUTING.md -├── Cargo.toml -├── Cargo.lock -├── initialize.js -├── package-lock.json -├── package.json -├── packages -├── public -├── src -│   ├── components -│   │   └── Card.astro -│   ├── env.d.ts -│   ├── layouts -│   │   └── Layout.astro -│   └── pages -│   └── index.astro -└── tsconfig.json -``` -The `contracts` are the same ones you walked through in the previous steps of the tutorial. Since we already deployed these contracts with aliases, we can reuse the generated contract ID files by copying them from the `soroban-hello-world/.stellar` directory into this project: +# For example, you could clone this repository +git clone https://github.com/stellar/soroban-examples -```sh -cp -R ../soroban-hello-world/.stellar/ .stellar ``` -## Generate an NPM package for the Hello World contract +Now copy files into the root of `soroban-hello-world`. -Before we open the new frontend files, let's generate an NPM package for the Hello World contract. This is our suggested way to interact with contracts from frontends. These generated libraries work with any JavaScript project (not a specific UI like React), and make it easy to work with some of the trickiest bits of Soroban, like encoding [XDR](../../learn/fundamentals/contract-development/types/fully-typed-contracts.mdx). +So how can you find other valid frontend templates? -This is going to use the CLI command `stellar contract bindings typescript`: +In GitHub, in the main search bar, search for `"soroban-template-"`. With the quotes. Here's a direct link to the search results: [github.com/search?q=%22soroban-template-%22](http://github.com/search?q=%22soroban-template-%22) -```bash -stellar contract bindings typescript \ - --network testnet \ - --contract-id hello_world \ - --output-dir packages/hello_world -``` +You can copy this approach for any other source code website, such as GitLab. -:::tip +How do you know if any of these are any good? Try them. Look at their source code. How many stars do they have? How active are their maintainers? None of these are perfect metrics, which is why a curated registry might be nice in the future. -Notice that we were able to use the contract alias, `hello_world`, in place of the contract id! +If none of them suit, then it might be time to... -::: +## Make your own template -This project is set up as an NPM Workspace, and so the `hello_world` client library was generated in the `packages` directory at `packages/hello_world`. +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, and is a part of the template initialization; bindings for the `hello world` smart contract are created. -We attempt to keep the code in these generated libraries readable, so go ahead and look around. Open up the new `packages/hello_world` directory in your editor. If you've built or contributed to Node projects, it will all look familiar. You'll see a `package.json` file, a `src` directory, a `tsconfig.json`, and even a README. +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. -## Generate an NPM package for the Increment contract +### 1. Initialize a SolidJS project -Though we can run `stellar contract bindings typescript` for each of our contracts individually, the [soroban-template-astro](https://github.com/stellar/soroban-astro-template) project that we used as our template includes a very handy `initialize.js` script that will handle this for all of the contracts in our `contracts` directory. +```bash -In addition to generating the NPM packages, `initialize.js` will also: +npx degit solidjs/templates/ts soroban-template-solid +cd soroban-template-solid +npm install +npm run dev -- Generate and fund our Stellar account -- Build all of the contracts in the `contracts` dir -- Deploy our contracts -- Create handy contract clients for each contract +``` -We have already taken care of the first three bullet points in earlier steps of this tutorial, so those tasks will be noops when we run `initialize.js`. +The basic SolidJS is now running on localhost port 3000. -### Configure initialize.js +#### Dependencies -We need to make sure that `initialize.js` has all of the environment variables it needs before we do anything else. Copy the `.env.example` file over to `.env`. The environment variables set in `.env` are used by the `initialize.js` script. +Most of the needed dependencies are already included by the SolidJS template, we just need to add three more: ```bash -cp .env.example .env -``` -Let's take a look at the contents of the `.env` file: +npm install dotenv glob util ``` -# Prefix with "PUBLIC_" to make available in Astro frontend files -PUBLIC_STELLAR_NETWORK_PASSPHRASE="Standalone Network ; February 2017" -PUBLIC_STELLAR_RPC_URL="http://localhost:8000/soroban/rpc" -STELLAR_ACCOUNT="me" -STELLAR_NETWORK="standalone" -``` +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. -This `.env` file defaults to connecting to a locally running network, but we want to configure our project to communicate with Testnet, since that is where we deployed our contracts. To do that, let's update the `.env` file to look like this: +#### Smart contract -```diff -# Prefix with "PUBLIC_" to make available in Astro frontend files --PUBLIC_STELLAR_NETWORK_PASSPHRASE="Standalone Network ; February 2017" -+PUBLIC_STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015" --PUBLIC_STELLAR_RPC_URL="http://localhost:8000/soroban/rpc" -+PUBLIC_STELLAR_RPC_URL="https://soroban-testnet.stellar.org:443" +Since we are going to interact with the smart contract from the `initialize.js` script, let’s copy the smart contract code to the root of the SolidJS template directory. Since the `initialize.js` script is calling Stellar CLI commands, it will only work if the smart contract code is in the same directory as the script. --STELLAR_ACCOUNT="me" -+STELLAR_ACCOUNT="alice" --STELLAR_NETWORK="standalone" -+STELLAR_NETWORK="testnet" -``` +The root directory should look like this: + +```text -:::info +├── contracts +│ └── hello_world +│ ├── src +│ │ └── lib.rs +│ └── Cargo.toml +│ └── Makefile +├── node_modules +├── packages +├── src +│ ├── App.tsx +│ └── index.tsx +├── .env +├── index.html +├── tsconfig.json +├── vite.config.ts +├── initialize.js +├── package.json +└── Cargo.toml -This `.env` file is used in the `initialize.js` script. When using the CLI, we can still use the network configuration we set up in the [Setup](../smart-contracts/getting-started/setup.mdx) step, or by passing the `--rpc-url` and `--network-passphrase` flags. +``` -::: +### 2. Environment variables -### Run `initialize.js` +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. -First let's install the Javascript dependencies: +These are the variables needed: ```bash -npm install -``` -And then let's run `initialize.js`: +STELLAR_NETWORK="testnet" +STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015" +STELLAR_RPC_URL="https://soroban-testnet.stellar.org" +STELLAR_ACCOUNT="my-user-name" -```bash -npm run init ``` -As mentioned above, this script attempts to build and deploy our contracts, which we have already done. The script is smart enough to check if a step has already been taken care of, and is a no-op in that case, so it is safe to run more than once. +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. -### Call the contract from the frontend +### 3. Initialize.js -Now let's open up `src/pages/index.astro` and take a look at how the frontend code integrates with the NPM package we created for our contracts. +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. -Here we can see that we're importing our generated `helloWorld` client from `../contracts/hello_world`. We're then invoking the `hello` method and adding the result to the page. +#### Definitions -```ts title="src/pages/index.astro" ---- -import Layout from "../layouts/Layout.astro"; -import Card from "../components/Card.astro"; -import helloWorld from "../contracts/hello_world"; -const { result } = await helloWorld.hello({ to: "you" }); -const greeting = result.join(" "); ---- +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); -

{greeting}

+// Define array to hold deployed smart contract info +var smartContracts = Array(); + +// Run exec commands asynchronously +const execAsync = promisify(exec); ``` -Let's see it in action! Start the dev server: +#### User -```bash -npm run dev -``` +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: -And open [localhost:4321](http://localhost:4321) in your browser. You should see the greeting from the contract! +```javascript +// ###################### Create User ######################## -You can try updating the `{ to: 'Soroban' }` argument. When you save the file, the page will automatically update. +function createUser() { + execSync( + `stellar keys generate --fund ${process.env.STELLAR_ACCOUNT} | true`, + ); +} +``` -:::info +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. -When you start up the dev server with `npm run dev`, you will see similar output in your terminal as when you ran `npm run init`. This is because the `dev` script in package.json is set up to run `npm run init` and `astro dev`, so that you can ensure that your deployed contract and your generated NPM pacakage are always in sync. If you want to just start the dev server without the initialize.js script, you can run `npm run astro dev`. +You can check the new user’s public key by running this CLI command: -::: +```bash -### What's happening here? +stellar keys public-key < my-user-name > -If you inspect the page (right-click, inspect) and refresh, you'll see a couple interesting things: +``` -- The "Network" tab shows that there are no Fetch/XHR requests made. But RPC calls happen via Fetch/XHR! So how is the frontend calling the contract? -- There's no JavaScript on the page. But we just wrote some JavaScript! How is it working? +With the public key, you can look up the account on [Stellar Expert](https://stellar.expert/explorer/testnet). -This is part of Astro's philosophy: the frontend should ship with as few assets as possible. Preferably zero JavaScript. When you put JavaScript in the [frontmatter](https://docs.astro.build/en/core-concepts/astro-components/), Astro will run it at build time, and then replace anything in the `{...}` curly brackets with the output. +#### Build contracts -When using the development server with `npm run dev`, it runs the frontmatter code on the server, and injects the resulting values into the page on the client. +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). -You can try building to see this more dramatically: +```javascript +// Remove all previous build files +function removeFiles(pattern) { + glob(pattern).forEach((entry) => rmSync(entry)); +} -```bash -npm run build +function buildAll() { + removeFiles(`${dirname}/target/wasm32v1-none/release/*.wasm`); + removeFiles(`${dirname}/target/wasm32v1-none/release/*.d`); + execSync(`stellar contract build`); + console.log("Build complete"); +} ``` -Then check the `dist` folder. You'll see that it built an HTML and CSS file, but no JavaScript. And if you look at the HTML file, you'll see a static "Hello Soroban" in the `

`. +The helper function `removeFiles` will delete any `wasm` or `d` files in the target directory. -During the build, Astro made a single call to your contract, then injected the static result into the page. This is great for contract methods that don't change, but probably won't work for most contract methods. Let's integrate with the `incrementor` contract to see how to handle interactive methods in Astro. --> +#### Deploy contracts -## Call the incrementor contract from the frontend +Now that the smart contract has been built, we can deploy the smart contract to the network, so we can invoke the smart contract functions from any client, such as our SolidJS template. -While `hello` is a simple view-only/read method, `increment` changes on-chain state. This means that someone needs to sign the transaction. So we'll need to add transaction-signing capabilities to the frontend. +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. -The way signing works in a browser is with a _wallet_. Wallets can be web apps, browser extensions, standalone apps, or even separate hardware devices. +```javascript +// Get smart contract name from filename +function filenameNoExtension(filename) { + return path.basename(filename, path.extname(filename)); +} -### Install Freighter Extension +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.STELLAR_ACCOUNT} --network ${process.env.STELLAR_NETWORK} --rpc-url ${process.env.STELLAR_RPC_URL} --network-passphrase "${process.env.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(), + }); -Right now, the wallet that best supports Soroban is [Freighter](../guides/freighter/README.mdx). It is available as a Firefox Add-on, as well as extensions for Chrome and Brave. Go ahead and [install it now](https://freighter.app). + console.log(`Deployed ${filenameNoExtension(wasm)}`); +} + +async function deployAll() { + console.log("Deploying all contracts"); + const wasmFiles = glob(`${dirname}/target/wasm32v1-none/release/*.wasm`); -Once it's installed, open it up by clicking the extension icon. If this is your first time using Freighter, you will need to create a new wallet. Go through the prompts to create a password and save your recovery passphrase. + for (const wasm of wasmFiles) { + await deploy(wasm); + } +} +``` -Go to Settings (the gear icon) → Preferences and toggle the switch to Enable Experimental Mode. Then go back to its home screen and select "Test Net" from the top-right dropdown. Finally, if it shows the message that your Stellar address is not funded, go ahead and click the "Fund with Friendbot" button. +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. -Now you're all set up to use Freighter as a user, and you can add it to your app. +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. -### Add the StellarWalletsKit and set it up +##### Create bindings -Even though we're using Freighter to test our app, there are more wallets that support signing smart contract transactions. To make their integration easier, we are using the `StellarWalletsKit` library which allows us support all Stellar Wallets with a single library. +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. -To install this kit we are going to include the next package: +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()`). -```shell -npm install @creit.tech/stellar-wallets-kit +```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); + } +} ``` -With the package installed, we are going to create a new simple file where our instantiated kit and simple state will be located. Create the file `src/stellar-wallets-kit.ts` and paste this: +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 generate the bindings. -```ts title="src/stellar-wallets-kit.ts" -import { - allowAllModules, - FREIGHTER_ID, - StellarWalletsKit, -} from "@creit.tech/stellar-wallets-kit"; +#### Import bindings -const SELECTED_WALLET_ID = "selectedWalletId"; +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. -function getSelectedWalletId() { - return localStorage.getItem(SELECTED_WALLET_ID); -} +The file is stored with the contract name as the file name, and with the `.ts` as the extension, e.g., `hello_world.ts`. -const kit = new StellarWalletsKit({ - modules: allowAllModules(), - network: import.meta.env.PUBLIC_STELLAR_NETWORK_PASSPHRASE, - // StellarWalletsKit forces you to specify a wallet, even if the user didn't - // select one yet, so we default to Freighter. - // We'll work around this later in `getPublicKey`. - selectedWalletId: getSelectedWalletId() ?? FREIGHTER_ID, -}); +```javascript +function importContract({ alias, wasm, contractid }) { + const outputDir = `${dirname}/src/contracts/`; -export const signTransaction = kit.signTransaction.bind(kit); + mkdirSync(outputDir, { recursive: true }); -export async function getPublicKey() { - if (!getSelectedWalletId()) return null; - const { address } = await kit.getAddress(); - return address; -} + const importContent = + `import * as Client from '${alias}';\n` + + `export default new Client.Client({\n` + + ` contractId: "${contractid}",\n` + + ` networkPassphrase: "${process.env.STELLAR_NETWORK_PASSPHRASE}",\n` + + ` rpcUrl: "${process.env.STELLAR_RPC_URL}",\n` + + `${ + process.env.STELLAR_NETWORK === "local" || "standalone" + ? ` allowHttp: true,\n` + : null + }` + + `});\n`; -export async function setWallet(walletId: string) { - localStorage.setItem(SELECTED_WALLET_ID, walletId); - kit.setWallet(walletId); -} + const outputPath = `${outputDir}/${alias}.ts`; + writeFileSync(outputPath, importContent); -export async function disconnect(callback?: () => Promise) { - localStorage.removeItem(SELECTED_WALLET_ID); - kit.disconnect(); - if (callback) await callback(); + console.log(`Created import for ${alias}`); } -export async function connect(callback?: () => Promise) { - await kit.openModal({ - onWalletSelected: async (option) => { - try { - await setWallet(option.id); - if (callback) await callback(); - } catch (e) { - console.error(e); - } - return option.id; - }, - }); +function importAll() { + smartContracts.forEach(importContract); } ``` -In the code above, we instantiate the kit with desired settings and export it. We also wrap some kit functions and add custom functionality, such as augmenting the kit by allowing it to remember which wallet options was selected between page refreshes (that's the `localStorage` bit). The kit requires a `selectedWalletId` even before the user selects one, so we also work around this limitation, as the code comment explains. You can learn more about how the kit works in [the StellarWalletsKit documentation](https://stellarwalletskit.dev/) - -Now we're going to add a "Connect" button to the page which will open the kit's built-in modal, and prompt the user to use their preferred wallet. Once the user picks their preferred wallet and grants permission to accept requests from the website, we will fetch the public key and the "Connect" button will be replaced with a message saying, "Signed in as [their public key]". - -Now let's add a new component to the `src/components` directory called `ConnectWallet.astro` with the following content: - -```html title="src/components/ConnectWallet.astro" -
-   -
- - -
- - - - +main().catch((e) => { + console.error("Initialization failed", e); + process.exit(1); +}); ``` -Some of this may look surprising. `