A quick, step-by-step guide to deploying the Fluxora stream contract to Stellar testnet.
-
Rust installed (v1.70+)
rustup --version rustup target add wasm32-unknown-unknown
-
Stellar CLI installed (install guide)
stellar --version
-
Testnet account with funds
- Create/fund via Stellar testnet faucet
- Funded account = Deployer account (holds the contract, cov gas fees)
-
Environment variables configured
cp .env.example .env # Then edit .env with: export STELLAR_SECRET_KEY="S..." # Deployer secret key export STELLAR_ADMIN_ADDRESS="G..." # Admin/treasury public key export STELLAR_TOKEN_ADDRESS="C..." # USDC or test token contract address export STELLAR_NETWORK="testnet" # (optional) export STELLAR_RPC_URL="https://soroban-testnet.stellar.org" # (optional)
cargo build --release -p fluxora_stream --target wasm32-unknown-unknownExpected output: target/wasm32-unknown-unknown/release/fluxora_stream.wasm (~150 KB)
The deployment script handles WASM upload, contract deployment, and init in one go:
source .env
bash script/deploy-testnet.shWhat it does:
- ✅ Validates env vars and CLI prerequisites
- ✅ Builds the WASM binary
- ✅ Uploads WASM to testnet (idempotent — skips if unchanged)
- ✅ Deploys contract instance (idempotent — skips if already deployed)
- ✅ Invokes
initto set token and admin - ✅ Saves contract ID to
.contract_idfor future use
Output: Contract ID will be saved to .contract_id file (example: CAHUB4AGDYVQ3G5T3B...)
If you prefer to deploy manually:
# Step 1: Upload WASM
WASM_ID=$(stellar contract upload \
--wasm target/wasm32-unknown-unknown/release/fluxora_stream.wasm \
--network testnet \
--source "$STELLAR_SECRET_KEY" \
--rpc-url https://soroban-testnet.stellar.org)
# Step 2: Deploy contract
CONTRACT_ID=$(stellar contract deploy \
--wasm-hash "$WASM_ID" \
--network testnet \
--source "$STELLAR_SECRET_KEY" \
--rpc-url https://soroban-testnet.stellar.org)
# Save for later
echo "$CONTRACT_ID" > .contract_idThe contract requires init to be called exactly once, setting the token address and admin.
The script calls init automatically at the end:
bash script/deploy-testnet.shIf you deployed manually or need to re-initialize:
CONTRACT_ID=$(cat .contract_id) # or use your deployed contract ID
stellar contract invoke \
--id "$CONTRACT_ID" \
--network testnet \
--source "$STELLAR_SECRET_KEY" \
--rpc-url https://soroban-testnet.stellar.org \
-- init \
--token "$STELLAR_TOKEN_ADDRESS" \
--admin "$STELLAR_ADMIN_ADDRESS"Note: init can only be called once. Calling it again will fail (by design).
After deployment, verify the contract is working:
Check that init succeeded by reading the contract config:
CONTRACT_ID=$(cat .contract_id)
stellar contract invoke \
--id "$CONTRACT_ID" \
--network testnet \
--source "$STELLAR_SECRET_KEY" \
--rpc-url https://soroban-testnet.stellar.org \
-- get_configExpected output: {"token": "C...", "admin": "G..."}
Create a sample stream to verify create_stream works:
stellar contract invoke \
--id "$CONTRACT_ID" \
--network testnet \
--source "$STELLAR_SECRET_KEY" \
--rpc-url https://soroban-testnet.stellar.org \
-- create_stream \
--sender "$STELLAR_ADMIN_ADDRESS" \
--recipient "GBRPYHIL2CI3WHZDTOOQFC6EB4CGQOFSNQB37HY5SKBRZGTAE3Z5MJGF" \
--deposit_amount 1000000 \
--rate_per_second 1000 \
--cliff_time 1700000000 \
--end_time 1800000000Expected output: Stream ID (e.g., 0) printed to console
stellar contract invoke \
--id "$CONTRACT_ID" \
--network testnet \
--source "$STELLAR_SECRET_KEY" \
--rpc-url https://soroban-testnet.stellar.org \
-- get_stream_state \
--stream_id 0Expected output:
{
"sender": "G...",
"recipient": "G...",
"deposit_amount": 1000000,
"rate_per_second": 1000,
"start_time": ...,
"cliff_time": ...,
"end_time": ...,
"withdrawn_amount": 0,
"status": "Active"
}- RPC URL:
https://soroban-testnet.stellar.org - Network Passphrase:
Test SDF Network ; September 2015 - Network ID:
testnet
The deployment script uses the RPC URL and network name automatically. No manual configuration needed unless you override with STELLAR_RPC_URL.
After deployment, you can view your contract on the Stellar testnet explorer:
- Stellar Expert Testnet
- Search for your Contract ID (from
.contract_id)
Problem: Deployment script exits with env var error.
Solution:
export STELLAR_SECRET_KEY="S..." # or source .envProblem: CLI is not installed or not in PATH.
Solution:
# Install Stellar CLI
# https://developers.stellar.org/docs/smart-contracts/getting-started/setup
# Verify installation
stellar --versionProblem: Contract deployment failed, usually due to insufficient funds or RPC timeout.
Solution:
- Verify your deployer account has funds:
stellar account info --network testnet --source "$STELLAR_SECRET_KEY" - Re-run the deployment script (idempotency should retry the deploy)
- Check RPC status:
curl https://soroban-testnet.stellar.org/health
Problem: init fails because it's already been called.
Solution:
- This is expected behavior.
initcan only run once. - Verify with
get_config. If it returns token and admin,initsucceeded.
Problem: Script skips WASM re-upload but you want to force a fresh upload.
Solution:
rm .wasm_id .wasm_id.sha256 .contract_id
bash script/deploy-testnet.shThis forces a fresh WASM upload and new contract deployment.
Problem: Testnet RPC is slow or unresponsive.
Solution:
- Check RPC status: https://status.stellar.org/
- Temporarily use alternative RPC (if available)
- Retry the deployment after a few minutes
This section describes what changed between CONTRACT_VERSION = 5 and CONTRACT_VERSION = 6, which DataKey entries were added or removed, the required admin migration steps, and the rollback procedure.
Cross-reference: See storage.md for the full DataKey discriminant table and evolution policy.
| Category | Change | Breaking? |
|---|---|---|
| New entrypoint | delegated_withdraw — relayer-submitted withdrawal with ed25519 signature committing to (stream_id, nonce, deadline, expected_minimum_amount) |
Additive |
| New entrypoint | get_delegated_nonce — view: current replay-protection nonce for a recipient |
Additive |
| New entrypoint | set_auto_claim — recipient registers a permissionless auto-claim destination; now validates against zero address |
Additive |
| New entrypoint | revoke_auto_claim, trigger_auto_claim, get_auto_claim_destination |
Additive |
| New constant | MAX_PAUSE_REASON_BYTES = 256 — pause-reason strings are now bounded |
Behaviour change: previously unbounded reasons now rejected if > 256 bytes |
| New error codes | InvalidSignature = 15, BelowMinimumAmount = 16, InvalidAutoClaimDestination = 17, PauseReasonTooLong = 18 |
Additive |
| New DataKey | DelegatedWithdrawNonce(Address) — discriminant 10, persistent |
Additive |
| Perf | batch_withdraw / batch_withdraw_to cache env.ledger().timestamp() before the loop |
No observable change |
| Discriminant | Variant | Storage type | Value type | Notes |
|---|---|---|---|---|
| 10 | DelegatedWithdrawNonce(Address) |
Persistent | u64 |
Per-recipient nonce; absent until first delegated_withdraw call; starts at 0 |
No existing DataKey entries were removed or reordered. All V5 persistent entries remain readable on a V6 instance.
Because V6 adds only new entrypoints and a new DataKey (append-only), no on-chain state transformation is required. The migration procedure is:
- Deploy the V6 contract instance (new
CONTRACT_ID). - Call
initon the new instance with the sametokenandadminas V5. - Verify version:
stellar contract invoke --id <NEW_ID> -- versionmust return6. - Verify config:
stellar contract invoke --id <NEW_ID> -- get_configmust match V5 config. - Announce migration to all integrators, wallets, and indexers with the new
CONTRACT_ID. - Allow in-flight streams to drain on the V5 instance before abandoning it (see below).
There is no migration_v5_to_v6 on-chain entrypoint because all stream state is local to the contract instance that created it and cannot be transferred between instances.
| Stream status | Recommended action |
|---|---|
Active |
Notify recipient. Let stream run to completion on V5, or cancel and recreate on V6. |
Paused |
Resume on V5, then cancel and recreate on V6 if desired. |
Cancelled |
Recipient must withdraw remaining accrued amount from V5 before it is abandoned. |
Completed |
No action needed; all funds already withdrawn. |
Minimum notice period: Announce the V5 deprecation date at least 14 days before abandoning the V5 instance. This gives recipients time to withdraw accrued funds.
TTL risk: V5 persistent entries expire after ~7 days of inactivity (PERSISTENT_BUMP_AMOUNT = 120_960 ledgers). If a stream has not been touched for 7 days, its storage entry may expire and become unrecoverable. Operators must ensure recipients are notified before TTL expiry.
If V6 must be rolled back:
- Stop routing new traffic to the V6
CONTRACT_IDimmediately. - Re-point integrations to the V5
CONTRACT_ID. - Verify V5 is still live: call
version()andget_config()on V5. - Drain any streams created on V6: cancel or let them complete, then recreate on V5 if needed.
- Announce rollback to all integrators.
V6 introduces no irreversible on-chain state changes that would prevent rollback to V5. The DelegatedWithdrawNonce entries on V6 are local to the V6 instance and have no effect on V5.
# 1. Build V6 WASM
cargo build --release -p fluxora_stream --target wasm32-unknown-unknown
# 2. Verify version constant
grep "CONTRACT_VERSION" contracts/stream/src/lib.rs
# Expected: pub const CONTRACT_VERSION: u32 = 6;
# 3. Deploy
source .env
bash script/deploy-testnet.sh
# 4. Verify
stellar contract invoke --id $(cat .contract_id) -- version
# Expected: 6
stellar contract invoke --id $(cat .contract_id) -- get_config
# Expected: {"token": "C...", "admin": "G..."}
# 5. Smoke-test new entrypoints
stellar contract invoke --id $(cat .contract_id) -- get_delegated_nonce \
--recipient <RECIPIENT_ADDRESS>
# Expected: 0- storage.md — DataKey discriminant table and evolution policy
- upgrade.md — CONTRACT_VERSION policy and breaking-change classification
- streaming.md — Full entrypoint reference including V6 additions
| Step | Command |
|---|---|
| Setup | cp .env.example .env → fill in env vars |
| Build | cargo build --release -p fluxora_stream --target wasm32-unknown-unknown |
| Deploy | bash script/deploy-testnet.sh |
| Verify | stellar contract invoke --id $(cat .contract_id) -- get_config |
| Test stream | stellar contract invoke --id $(cat .contract_id) -- create_stream ... |
| Approve tokens | stellar contract invoke --id $TOKEN_ID -- approve --from $USER --spender $CONTRACT --amount $AMT --expiration_ledger $EXP |
Fluxora uses an allowance-based model (via transfer_from) to pull tokens from your wallet when creating or topping up a stream. This means you must explicitly approve the contract to spend tokens on your behalf.
Before creating a stream, you can check if you've already approved the contract:
# Replace <TOKEN_ID>, <SENDER_ADDRESS>, and <CONTRACT_ID>
stellar contract invoke \
--id "<TOKEN_ID>" \
--network testnet \
--source "$STELLAR_SECRET_KEY" \
-- allowance \
--from "<SENDER_ADDRESS>" \
--spender "<CONTRACT_ID>"If the allowance is insufficient, grant approval to the contract. Note that you must specify an expiration ledger.
# Approve contract to spend 1,000,000 tokens (e.g. 1 USDC if 6 decimals)
# Expiration ledger should be sufficiently far in the future (e.g. current + 10,000)
stellar contract invoke \
--id "<TOKEN_ID>" \
--network testnet \
--source "$STELLAR_SECRET_KEY" \
-- approve \
--from "<SENDER_ADDRESS>" \
--spender "<CONTRACT_ID>" \
--amount 1000000 \
--expiration_ledger 1000000Once approved, you can call create_stream normally. The contract will pull the exact deposit_amount and the allowance will be consumed accordingly.
Tip
Most modern Soroban wallets (like Freighter) handle this two-step process automatically by prepending an approve operation to your transaction bundle.
After successful deployment:
- Fund test accounts for stream recipients via testnet faucet
- Create streams with realistic test data (senders, recipients, amounts, durations)
- Monitor accrual by calling
get_stream_stateat different times - Test withdrawals via the
withdrawmethod - Pause/resume/cancel streams to verify state transitions