Step-by-step instructions for deploying Soroban contracts and the frontend across all environments.
The Stellar CLI is required to generate identities, fund accounts, and deploy contracts.
# Install Stellar CLI
cargo install --locked stellar-cli --features opt
# Verify installation
stellar --versionExpected output:
stellar 21.x.x
Create a new identity for contract deployment:
# Generate a new identity (interactive)
stellar keys generate --global deployer
# Or generate non-interactively
stellar keys generate deployer --network testnetExpected output:
Created identity "deployer" with public key: GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
The secret key is stored securely in ~/.stellar/keys.yaml (never commit this file).
Use Friendbot to fund your testnet account with 10,000 XLM:
# Fund the deployer account
stellar keys fund deployer --network testnetExpected output:
Funded account GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX with 10000 XLM
Verify the account is funded:
stellar account info --source-account deployer --network testnetExpected output:
Account ID: GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Sequence: 0
Balances:
10000.0000000 XLM
Check that your identity is properly configured:
stellar keys listExpected output:
deployer (testnet)
| Environment | Network | RPC URL |
|---|---|---|
local |
Standalone | http://localhost:8000 |
testnet |
Test SDF Network | https://soroban-testnet.stellar.org |
mainnet |
Public Global Stellar | https://soroban.stellar.org |
Copy and configure environment variables:
cp .env.example .envVITE_STELLAR_NETWORK=local
VITE_SOROBAN_RPC_URL=http://localhost:8000
VITE_HORIZON_URL=http://localhost:8001
VITE_NETWORK_PASSPHRASE="Standalone Network ; February 2017"VITE_STELLAR_NETWORK=testnet
VITE_SOROBAN_RPC_URL=https://soroban-testnet.stellar.org
VITE_HORIZON_URL=https://horizon-testnet.stellar.org
VITE_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"VITE_STELLAR_NETWORK=mainnet
VITE_SOROBAN_RPC_URL=https://soroban.stellar.org
VITE_HORIZON_URL=https://horizon.stellar.org
VITE_NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015"scripts/deploy.sh uses a Stellar CLI identity (key pair) as the transaction source account. Before deploying, ensure the identity exists on your machine.
stellar keys show defaultIf the command fails with "not found", create it:
# Generate a new key pair and store it globally
stellar keys generate --global default
# Or import an existing secret key
stellar keys add default --secret-key
# (you will be prompted to enter the secret key)Pass --identity <name> to use a key other than default:
./scripts/deploy.sh testnet --identity my-deployer
./scripts/deploy.sh local token --identity alicedeploy.sh validates the identity before building any contracts and prints setup guidance if it is missing.
stellar keys fund default --network testnet# Start local Stellar node
./scripts/local-net.sh start
# Build and deploy all contracts
./scripts/deploy.sh local
# Deploy a single contract
./scripts/deploy.sh local token
# Start frontend dev server
npm run devVerify your deployer account has sufficient XLM:
stellar account info --source-account deployer --network testnetIf the account shows 0 XLM, fund it via Friendbot:
stellar keys fund deployer --network testnetDeploy all contracts:
./scripts/deploy.sh testnetDeploy a single contract:
./scripts/deploy.sh testnet token
# or
./scripts/deploy.sh testnet escrowExpected output:
Building token contract...
Deploying token contract to testnet...
Contract ID: CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Admin: GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Save the contract IDs printed to stdout — you'll need them in .env:
VITE_TOKEN_CONTRACT_ID=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
VITE_ESCROW_CONTRACT_ID=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXVerify the contract is deployed and responsive:
stellar contract info --id <CONTRACT_ID> --network testnetExpected output:
Contract ID: CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
WASM Hash: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
After deploying contracts, run the initialization script to call each contract's initialize function:
# Populate .contract-ids with deployed IDs (one per line: name=CONTRACT_ID)
echo "token=CABC..." >> .contract-ids
echo "escrow=CDEF..." >> .contract-ids
# Initialize all contracts on the current network
./scripts/initialize.sh testnet # or local / mainnetEnvironment variable overrides:
| Variable | Default | Description |
|---|---|---|
INIT_FN |
initialize |
Function name to invoke |
SOURCE_ACCOUNT |
default |
Stellar key to sign transactions |
CONTRACT_IDS_FILE |
.contract-ids |
Path to contract ID list |
INIT_ARGS_<NAME> |
(none) | Extra CLI args for a specific contract, e.g. INIT_ARGS_TOKEN="--admin GABC..." |
The script is idempotent: contracts that return an "already initialized" error are reported as skipped rather than failures.
⚠️ Mainnet deployments are irreversible. Complete testnet validation first.
# Ensure deployer account has sufficient XLM for fees
stellar keys generate --global deployer-mainnet
# Dry-run simulation (no broadcast)
stellar contract deploy --wasm <path>.wasm \
--rpc-url https://soroban.stellar.org \
--network-passphrase "Public Global Stellar Network ; September 2015" \
--source-account deployer-mainnet \
--simulate-only
# Deploy
./scripts/deploy.sh mainnetnpm run build # outputs to dist/docker build -f docker/Dockerfile --target prod -t soroban-kit:prod .
docker run -p 80:80 soroban-kit:proddocker compose -f docker/docker-compose.yml up --buildnpm run build
# Upload dist/ to your static host
# Set environment variables in the host's dashboard (not in .env)The GitHub Actions workflow at .github/workflows/ci.yml runs on every push:
npm ci— install dependenciesnpm run lint— ESLint checksnpm test— Vitest unit testscargo test— Rust contract testsstellar contract build— WASM compilation check
To add automated deployment, extend .github/workflows/ci.yml:
- name: Deploy to testnet
if: github.ref == 'refs/heads/main'
run: ./scripts/deploy.sh testnet
env:
STELLAR_SECRET_KEY: ${{ secrets.STELLAR_SECRET_KEY }}Run the docs check script to validate documentation coverage and regenerate the docs report:
node scripts/check-docs.mjs
# Outputs: docs-report.jsonTo regenerate environment-specific config snippets automatically:
node scripts/generate-guides.mjs
# Outputs: docs/integration-guide.md, docs/deployment-guide.md- All contract tests pass:
cargo test - Frontend tests pass:
npm test - Lint passes:
npm run lint -
.envis configured for the target network - Deployer account is funded
- Contract IDs from previous deployments are recorded
- Contract health verified — run
./scripts/check-contract-ids.sh(see §13) - Contract responds to
simulatecalls - Frontend connects to the correct RPC endpoint
- Admin functions are restricted to the correct address
- Event emission verified via Stellar Laboratory
- Contract ID saved to
.envand committed (not the secret key)
- Store
STELLAR_SECRET_KEYonly in CI secrets, never in.envcommitted to git - Use separate deployer accounts per environment (local / testnet / mainnet)
- Verify WASM hash after deployment:
stellar contract info --id <CONTRACT_ID> - Enable
set_adminonly during initialization; lock it down immediately after - Audit contract code with
cargo auditbefore mainnet deployment:cargo install cargo-audit cargo audit
- Use
--simulate-onlyto estimate fees before broadcasting
- Build contracts with
--releaseflag (default indeploy.sh) - Minimize contract storage reads — cache values in
Env::storage().instance() - Use
TTLextensions for long-lived contract data to avoid expiry - Profile WASM size:
wasm-opt -Oz input.wasm -o output.wasm - Frontend: set
Cache-Control: max-age=31536000for hashed static assets
Both the Token and Escrow contracts enforce a two-step upgrade process when
built with the upgradeable / pausable feature flags. A minimum delay of
UPGRADE_DELAY_LEDGERS (17 280 ledgers ≈ 24 hours at 5 s/ledger) is enforced
between proposing and executing a WASM upgrade.
stellar contract invoke \
--id <CONTRACT_ID> \
--source-account <ADMIN_KEY> \
--network testnet \
-- propose_upgrade \
--wasm_hash <NEW_WASM_HASH>This stores the hash and a ready_after ledger number on-chain and emits an
upgrade_proposed event. Announce the upgrade publicly so users have time to
review and exit if needed.
stellar contract invoke \
--id <CONTRACT_ID> \
--source-account <ADMIN_KEY> \
--network testnet \
-- execute_upgradeThe call will fail with NotAuthorized / Unauthorized if the current ledger
is still before ready_after. Once executed, an upgrade_executed event is
emitted and the WASM is replaced atomically.
stellar contract info --id <CONTRACT_ID> --network testnetCompare the reported WASM hash against the expected value before announcing the upgrade as complete.
- Never skip the timelock on mainnet. The delay gives users time to react.
- Gate
propose_upgradebehind a multi-sig or governance vote for production. - Rehearse the full upgrade flow on testnet before executing on mainnet.
| Problem | Solution |
|---|---|
stellar: command not found |
Install Stellar CLI: cargo install --locked stellar-cli --features opt |
Identity "deployer" not found |
Generate identity: stellar keys generate --global deployer |
Account not found |
Fund account via Friendbot: stellar keys fund deployer --network testnet |
Insufficient balance for fees |
Verify funding: stellar account info --source-account deployer --network testnet |
Friendbot rate limit exceeded |
Wait 5 minutes and retry, or use a different account |
Invalid network passphrase |
Ensure .env has correct VITE_NETWORK_PASSPHRASE for the target network |
| Problem | Solution |
|---|---|
wasm32 target missing |
rustup target add wasm32-unknown-unknown |
| Deploy fails: insufficient fee | Increase fee in deploy.sh or fund account |
| Local node unhealthy | ./scripts/local-net.sh reset then retry |
| Contract already initialized | Deploy a fresh contract; initialization is one-time |
| Frontend shows wrong network | Check VITE_STELLAR_NETWORK in .env |
| CORS errors from RPC | Use a proxy or the official RPC endpoints |
After deploying, confirm that every contract in .contract-ids is alive:
./scripts/check-contract-ids.shThe script reads .contract-ids (format: name=<CONTRACT_ID>), invokes get_state
on each contract, and categorises results:
| Status | Meaning |
|---|---|
| ALIVE | Contract responded normally |
| EXPIRED TTL | Entry has expired; extend TTL with stellar contract extend |
| UNREACHABLE | Contract not found or RPC error; re-deploy if necessary |
Override the default file or network:
./scripts/check-contract-ids.sh .contract-ids.testnet
STELLAR_NETWORK=mainnet ./scripts/check-contract-ids.shThe script exits non-zero if any contract is expired or unreachable, making it suitable for use in CI/CD pipelines.