In this tutorial, we’ll walk through the process of deploying a fungible token on the Stellar network using Soroban smart contracts, and then build a decentralized application to interact with it. We’ll cover everything from setting up your development environment to creating a user interface for token transfers.

Prerequisites
Before we begin, you’ll need:
- Basic familiarity with Rust and React
- A code editor such as vscode
- Command line interface/terminal
- Node.js and npm installed
- Rust and cargo installed
Soroban Tokens
A fungible token is a digital asset where each unit is identical and interchangeable with another unit of the same token. This differs from non-fungible tokens, where each token is unique. Our implementation will follow standard token patterns similar to ERC20 on Ethereum, including functions like:
mint
Enables the owner to mint tokens to any wallettransfer
Send tokens directly between walletsapprove
Allow a contract to spend tokens on your behalftransfer_from
Let an approved contract move tokens
Note that Soroban/Stellar tokens have 7 decimals as standard and there is a max of 18 decimals set at the contract level. The balances are held within an i128 integer which would make the max possible value:
170,141,183,460,469,231,731,687,303,715,884,105,727
1. Setting Up the Development Environment
Add WebAssembly target support:
rustup target add wasm32-unknown-unknown
Install the Stellar CLI:
cargo install --locked stellar-cli --features opt
2. Building and Deploying the Token Contract
Test the contract:
cargo test
Build the WebAssembly binary:
cargo build --target wasm32-unknown-unknown --release
Create a wallet for deployment:
stellar keys generate --global james --network testnet
Get your wallet address:
stellar keys address james
Deploy the contract:
stellar contract deploy --wasm target/wasm32-unknown-unknown/release/soroban_token_contract.wasm --source james --network testnet
Save the contract ID returned from this command, you’ll need it later.
3. Initializing the Token
Initialize the token with basic parameters:
stellar contract invoke --id <CONTRACT_ID> --source-account james --network testnet -- initialize --admin <YOUR_ADDRESS> --decimal 7 --name '' --symbol '""'
Mint some tokens:
stellar contract invoke --id <CONTRACT_ID> --source-account james --network testnet -- mint --to <YOUR_ADDRESS> --amount <AMOUNT>
Note: Remember to consider decimals when setting amounts. For 7 decimals, multiply your desired amount by 10^7 or add seven zeros at the end.
4. Testing Token Transfers
Create another test wallet:
stellar keys generate --global alice --network testnet
Transfer tokens:
stellar contract invoke --id <CONTRACT_ID> --source-account james --network testnet -- transfer --from <YOUR_ADDRESS> --to <RECIPIENT_ADDRESS> --amount <AMOUNT>
Check balances:
stellar contract invoke --id <CONTRACT_ID> --source-account james --network testnet -- balance --id <ADDRESS
>
Building the dApp
Now let’s create a user interface to interact with our token.

We’ll use React and the Freighter wallet.
Set up your React project and install dependencies:
npm install stellar-sdk @stellar/freighter-api
Create the main App component (App.js):
The full code for this is available at:
https://github.com/jamesbachini/Token-dApp/blob/main/dapp/src/App.js
import './App.css';
import React, { useState, useEffect } from 'react';
import * as StellarSdk from 'stellar-sdk';
import freighterApi from "@stellar/freighter-api";
const App = () => {
const [publicKey, setPublicKey] = useState('');
const [balance, setBalance] = useState('');
const [to, setTo] = useState('');
const [value, setValue] = useState('');
const [error, setError] = useState('');
const rpc = new StellarSdk.SorobanRpc.Server('https://soroban-testnet.stellar.org');
const contractId = 'CCOCB24RH7R2TKF4QVS4J6GBLZ5IZK4FXWQWMQ6GYRGHNUFPW53VJOFU';
const contract = new StellarSdk.Contract(contractId);
const handleSend = async (e) => {
e.preventDefault();
try {
const inputFrom = StellarSdk.nativeToScVal(publicKey, { type: "address" });
const inputTo = StellarSdk.nativeToScVal(to, { type: "address" });
const inputValue = StellarSdk.nativeToScVal(value, { type: "i128" });
const account = await rpc.getAccount(publicKey);
const network = await freighterApi.getNetwork()
const tx = new StellarSdk.TransactionBuilder(account, {
fee: StellarSdk.BASE_FEE,
networkPassphrase: StellarSdk.Networks.TESTNET,
})
.addOperation(contract.call("transfer", inputFrom, inputTo, inputValue))
.setTimeout(30)
.build();
console.log('tx.toXDR()',tx.toXDR());
//const preparedTx = await rpc.prepareTransaction(tx);
const signedTX = await freighterApi.signTransaction(tx.toXDR(), network);
console.log('signedTransaction', signedTX);
const preparedTx = StellarSdk.TransactionBuilder.fromXDR(signedTX, StellarSdk.Networks.TESTNET);
console.log('preparedTx',preparedTx);
const txResult = await rpc.sendTransaction(preparedTx);
console.log('txResult', txResult);
setTo('');
setValue('');
} catch (err) {
setError('Failed to set value: ' + err.message);
}
};
const connectWallet = async () => {
const isAppConnected = await freighterApi.isConnected();
if (!isAppConnected.isConnected) {
alert("Please install the Freighter wallet");
window.open('https://freighter.app');
}
const accessObj = await freighterApi.requestAccess();
if (accessObj.error) {
alert('Error connecting freighter wallet');
} else {
setPublicKey(accessObj.address);
}
}
const getBalance = async () => {
if (!publicKey) {
console.log('No public key to fetch balance');
return;
}
//console.log('publicKey', publicKey);
try {
const inputAddressID = StellarSdk.nativeToScVal(publicKey, { type: "address" });
//console.log('inputAddressID',inputAddressID);
const account = await rpc.getAccount(publicKey);
//console.log('account', account)
const tx = new StellarSdk.TransactionBuilder(account, {
fee: StellarSdk.BASE_FEE,
networkPassphrase: StellarSdk.Networks.TESTNET,
})
.addOperation(contract.call("balance", inputAddressID))
.setTimeout(30)
.build();
//console.log('tx', tx);
rpc.simulateTransaction(tx).then((sim) => {
const decoded = StellarSdk.scValToNative(sim.result?.retval);
//console.log('decoded', decoded);
setBalance(decoded.toString());
});
} catch (err) {
setError('Failed to get value: ' + err.message);
}
}
useEffect(() => {
connectWallet();
}, []);
getBalance();
return (
<div className="app">
<h1 className="app-title">Soroban Token dApp</h1>
<div className="wallet">Wallet: <span className="public-key">{publicKey}</span></div>
<div className="balance">Balance: <span className="balance-amount">{balance}</span></div>
{error && (
<div className="error-message">
<strong>Error: </strong>
<span>{error}</span>
</div>
)}
<div className="section">
<h2 className="section-title">Send Tokens</h2>
<form onSubmit={handleSend} className="form">
<input
type="text"
value={to}
onChange={(e) => setTo(e.target.value)}
placeholder="To Address"
className="input"
/>
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Value"
className="input"
/>
<button type="submit" className="button">Send</button>
</form>
</div>
</div>
);
};
export default App;
We can build this for an external host by running npm run build
6. Using the Token in Freighter Wallet
- Open Freighter wallet
- Go to “Manage Assets”
- Add a new asset manually using your contract ID
- The token will appear under unverified assets
- Refresh to see your balance after transfers
7. Testing the Complete System
Start your React development server:
npm start

- Connect your Freighter wallet
- Try sending tokens to another address
- Verify the transaction in Stellar Expert
You can also see the token interface within Stellar.expert which should look familiar if you’ve worked with digital assets in the past.

You now have a fully functional token on the Stellar network with a dApp interface. This foundation can be extended with additional features like:
- Token burning
- Allowance management
- Enhanced transaction history
- Advanced error handling
Remember to keep your contract ID and deployment details safe, and always follow security best practices when handling real assets. Code is for experimental purposes only, has not been security audited and is not fit for production use.