Introduction: Web3 Backends for Modern Developers
This tutorial will show you how to leverage a React frontend with a Rust smart contract for Soroban as a powerful alternative to traditional database backed applications.
- React is the worlds most popular Javascript framework for frontend development
- Stellar smart contract platform offers developers a way to build permissionless, decentralized applications with the performance and safety guarantees of Rust.
Why Web3?
Before we dive in, let's talk about why you might want to consider this approach:
- Decentralization: Your data lives on the blockchain rather than a centralized server
- Immutability: Once written, your data can't be tampered with
- Transparency: All operations are public and verifiable
- Safety: Rust's memory safety guarantees help prevent common programming errors
- Performance: Soroban is designed for scalability and efficiency
Stellar has been around since 2014 and has proven itself as a reliable blockchain network with low transaction costs and fast settlement times.
Prerequisites
- Basic knowledge of Rust (or willingness to learn!)
- Familiarity with web development concepts
- Node, Rust & Git installed
Setting Up Your Environment
Let's start by getting your development environment ready. We'll need the Soroban CLI to interact with the Stellar network and deploy our contracts.
# Install Rust if you haven't already
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Add the WebAssembly target
rustup target add wasm32-unknown-unknown
# Install Soroban CLI
cargo install --locked soroban-cli
# Verify installation
soroban --version
Project Structure: Stellar Soroban Boilerplate
We'll be working with a boilerplate that provides a solid foundation for building web3 applications. The repository includes both smart contract code written in Rust and a React frontend interface to interact with it.
All the code for this tutorial is available at: https://github.com/jamesbachini/Stellar-Soroban-Boilerplate
Let's clone the repository:
git clone https://github.com/jamesbachini/Stellar-Soroban-Boilerplate
cd Stellar-Soroban-Boilerplate
The project structure looks like this:
Stellar-Soroban-Boilerplate/
├── soroban/contracts/src
│ └── lib.rs # A simple key-value store contract
│ └── test.rs # Unit tests for the contract
├── src/
│ └── App.js # Frontend example
└── README.md
Understanding the Key-Value Store Contract
At the heart of our Web3 backend is a key-value store contract. This serves as a simple database replacement that stores data on the Stellar blockchain instead of a traditional database server.
Let's look at the key parts of this contract:
#![no_std]
use soroban_sdk::{contract, contractimpl, Env, symbol_short, Symbol};
#[contract]
pub struct StarterContract;
#[contractimpl]
impl StarterContract {
pub fn set(env: &Env, key: Symbol, value: Symbol) {
env.storage().persistent().set(&key, &value);
env.events().publish((symbol_short!("SET"), &key), &value);
}
pub fn get(env: &Env, key: Symbol) -> Option<Symbol> {
env.storage().persistent().get(&key)
}
pub fn remove(env: &Env, key: Symbol) {
env.storage().persistent().remove(&key);
}
}
mod test;
This contract allows any user to:
- Store data with
set(key, value)
- Retrieve data with
get(key)
The set function stores the value on-chain and also fires an event which can be monitored on a frontend for real-time updates.
Building and Deploying Your Contract
Let's build and deploy our key-value store contract to the Soroban network:
cargo build --target wasm32-unknown-unknown --release
stellar keys generate mywallet
stellar keys fund mywallet
stellar contract deploy --wasm target/wasm32-unknown-unknown/release/StarterContract.wasm --source YourWalletAlias --network testnet
The stellar keys commands generates a new wallet address or keypair and then funds it with testnet XLM to pay the deployment costs. You should see something like this:
Contract deployed successfully!
Contract ID: CABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF12345678
Take note of this Contract ID - You can view it at: https://stellar.expert/explorer/testnet
You'll need it later for the frontend to interact with your contract!
The Web3 Frontend
Now that we have our contract deployed, let's look at how to interact with it from a web application. The boilerplate includes a simple frontend that demonstrates this.
First, let's set up a react dev environment:
npm install
npm run dev
Now let's edit src/App.js and modify lines 13, 15 & 16 to replace the contract ID and wallet keys which you can get via the stellar keys tool.
Let's examine the set function:
const handleSet = async (e) => {
e.preventDefault();
try {
const inputKey = StellarSdk.nativeToScVal(key, { type: "symbol" });
const inputValue = StellarSdk.nativeToScVal(value, { type: "symbol" });
const account = await rpc.getAccount(accountPublicKey);
const tx = new StellarSdk.TransactionBuilder(account, {
fee: StellarSdk.BASE_FEE,
networkPassphrase: StellarSdk.Networks.TESTNET,
})
.addOperation(contract.call("set", inputKey, inputValue))
.setTimeout(30)
.build();
const preparedTx = await rpc.prepareTransaction(tx);
preparedTx.sign(accountKeypair);
const txResult = await rpc.sendTransaction(preparedTx);
console.log('txResult', txResult);
setKey('');
setValue('');
} catch (err) {
setError('Failed to set value: ' + err.message);
}
};
The handleSet
function first prevents the default form submission behavior and converts the key and value from form inputs into a special Contract Value format as symbol types. It then retrieves the user's Stellar account and constructs a new transaction using the Stellar SDK. This transaction includes an operation to call the "set" method on a smart contract, passing in the converted key and value parameters.
After setting a timeout and building the transaction, it's prepared for submission, signed using the user's keypair, and sent to the Stellar network.
Note that when updating on-chain we have to pay a tx fee to modify the state and cover network/storage costs. When reading data there is no cost and we can just simulate the tx as we will see in the next snippet for the get function.
const handleGet = async (e) => {
e.preventDefault();
try {
const inputGetKey = StellarSdk.nativeToScVal(getKey, { type: "symbol" });
const account = await rpc.getAccount(accountPublicKey);
const tx = new StellarSdk.TransactionBuilder(account, {
fee: StellarSdk.BASE_FEE,
networkPassphrase: StellarSdk.Networks.TESTNET,
})
.addOperation(contract.call("get", inputGetKey))
.setTimeout(30)
.build();
rpc.simulateTransaction(tx).then((sim) => {
const decoded = StellarSdk.scValToNative(sim.result?.retval);
setGetValue(decoded);
});
} catch (err) {
setError('Failed to get value: ' + err.message);
}
};
In this function we are using the rpc.simulateTransaction function to ask our RPC node which is our entry point to the network to return the data from it's copy of the blockchain state.
This data is decoded and returned to be displayed on the frontend.
Next Steps
While our example uses a simple key-value store, you can build much more sophisticated data models. Here is an example of a function that has access control meaning only a certain account can update the ledger.
In this example the admin: Address
variable would be set in the constructor or somewhere else and then we check that they are the ones that are calling the set function so that only they can update data.
#[contractimpl]
pub fn set(env: &Env, key: Symbol, value: Symbol) {
let store = env.storage().persistent();
let admin: Address = store.get(&symbol_short!("admin")).unwrap();
admin.require_auth();
store.set(&key, &value);
env.events().publish((symbol_short!("SET"), &key), &value);
}
Handling Data Limitations
Blockchains in their current form are good for storing small amounts of critical data. Scaling efforts are constantly pushing the boundaries of what we can do within a smart contract.
Here are some strategies for managing larger datasets:
- Store Hashes: For large files or datasets, store only a hash or reference on-chain, with the full data in IPFS or a centralized service
- Paginate Data: If you have collections, implement pagination to retrieve data in chunks. Avoid iterating over an array of unknown size i.e. user data
- Use Events: For historical data, use blockchain events rather than storing everything. These can be indexed and referred back to as required.
For web developers looking to step into Web3, Soroban provides a gentle learning curve with familiar concepts and excellent documentation. The Stellar network efficiency makes it practical for real-world applications beyond simple experiments.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.