Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions docs/monitoring-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@ The script requires Soroban CLI, `jq`, and the matching

1. the configured RPC endpoint is reachable;
2. `progress-tracker`, `learn-token`, and `credential-nft` report initialized;
3. each contract returns a configured admin; and
4. `learn-token.total_supply` can be read.
3. each contract returns a configured admin;
4. pause states across all contracts (warning on paused operations);
5. pending admin transfer status across all contracts;
6. cross-contract wiring (`learn-token` and `credential-nft` point to `progress-tracker`);
7. `learn-token.total_supply` and admin balance;
8. `credential-nft` total credential and revocation metrics; and
9. `progress-tracker` course and enrollment metrics.

Run this check after every deployment, initialization, upgrade, configuration
change, and incident recovery. Schedule it regularly and alert on a non-zero
Expand Down
7 changes: 7 additions & 0 deletions docs/upgrade-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ soroban contract invoke \
--new_wasm_hash "$NEW_WASM_HASH"
```

Alternatively, run the automated upgrade script with `--multisig`:
```bash
./scripts/upgrade.sh testnet --multisig --co-signer "$ADMIN_2_ADDRESS"
# or upgrade only learn-token:
./scripts/upgrade.sh testnet learn-token --multisig --co-signer "$ADMIN_2_ADDRESS"
```

### Option C: Using Deploy Script

> [!NOTE]
Expand Down
290 changes: 259 additions & 31 deletions scripts/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,17 @@ set -euo pipefail
# ──────────────────────────────────────────────────────────────────────────────
#
# Usage:
# ./scripts/deploy.sh testnet
# ./scripts/deploy.sh mainnet
# ./scripts/deploy.sh testnet # Deploy to testnet with cost confirmation
# ./scripts/deploy.sh mainnet # Deploy to mainnet with cost confirmation
# ./scripts/deploy.sh testnet --confirm # Skip cost confirmation prompt (#602)
# ./scripts/deploy.sh testnet --resume # Resume from partial failure (#603)
# ./scripts/deploy.sh testnet --estimate-only # Show cost estimation and exit (#602)
#
# Flags:
# --confirm Skip confirmation prompt after showing deployment cost estimation
# --resume Resume deployment from a previous partial deployment failure
# --estimate-only Display pre-deployment cost estimation and exit
# -h, --help Display this help message
#
# Prerequisites:
# - soroban CLI installed (v21+)
Expand All @@ -17,7 +26,46 @@ set -euo pipefail
# - Sufficient XLM for deployment fees
# ──────────────────────────────────────────────────────────────────────────────

NETWORK="${1:-testnet}"
NETWORK="testnet"
CONFIRM=false
RESUME=false
ESTIMATE_ONLY=false

while [ "$#" -gt 0 ]; do
case "$1" in
--confirm)
CONFIRM=true
shift
;;
--resume)
RESUME=true
shift
;;
--estimate-only)
ESTIMATE_ONLY=true
shift
;;
testnet|mainnet)
NETWORK="$1"
shift
;;
-h|--help)
echo "Usage: $0 [testnet|mainnet] [--confirm] [--resume] [--estimate-only]"
echo ""
echo "Options:"
echo " --confirm Skip interactive confirmation prompt after cost estimation (#602)"
echo " --resume Resume deployment from previous partial state (#603)"
echo " --estimate-only Show estimated deployment costs and exit (#602)"
echo " -h, --help Show this help message"
exit 0
;;
*)
echo "Error: Unknown argument '$1'."
echo "Usage: $0 [testnet|mainnet] [--confirm] [--resume] [--estimate-only]"
exit 1
;;
esac
done

if [ "$NETWORK" = "testnet" ]; then
RPC_URL="https://soroban-testnet.stellar.org:443"
Expand All @@ -30,9 +78,9 @@ else
exit 1
fi

# Verify secret key is available
# Verify secret key is available (not strictly required if only estimating costs)
STELLAR_SECRET_KEY="${STELLAR_SECRET_KEY:-${SOROBAN_SECRET_KEY:-}}"
if [ -z "${STELLAR_SECRET_KEY:-}" ]; then
if [ -z "${STELLAR_SECRET_KEY:-}" ] && [ "$ESTIMATE_ONLY" = false ]; then
echo "Error: STELLAR_SECRET_KEY environment variable is not set."
echo "Export your Stellar secret key before running this script."
exit 1
Expand Down Expand Up @@ -60,19 +108,121 @@ echo "RPC reachable."
echo ""

DEPLOY_FILE="deployments-${NETWORK}.json"
if [ -f "$DEPLOY_FILE" ]; then
PARTIAL_STATE_FILE=".deploy-partial-${NETWORK}.json"

if [ -f "$DEPLOY_FILE" ] && [ "$RESUME" = false ]; then
echo "Error: Deployment already exists in $DEPLOY_FILE"
echo "To redeploy, please remove this file first."
echo "To redeploy, please remove or back up this file first."
exit 1
fi

# Check for unresumed partial state file
if [ -f "$PARTIAL_STATE_FILE" ] && [ "$RESUME" = false ]; then
echo "Warning: A previous partial deployment was found in $PARTIAL_STATE_FILE."
echo "Run with '--resume' to continue from the partial state:"
echo " $0 $NETWORK --resume $([ "$CONFIRM" = true ] && echo "--confirm")"
echo "Or remove $PARTIAL_STATE_FILE to start a fresh deployment."
exit 1
fi

# Build all contracts
# Build all contracts first so bytecode sizes are known for estimation and deployment
echo "[1/4] Building contracts..."
cargo build --release --target wasm32-unknown-unknown --workspace

# Shrink WASM before upload: install fees and code rent scale with size (#344).
# Set STRIP_SPEC_DOCS=1 to also strip doc comments from the embedded spec.
"$(dirname "$0")/optimize-wasm.sh"
echo " Build complete."
echo ""

# ── Deployment Cost Estimation (#602) ─────────────────────────────────────────

WASM_DIR="target/wasm32-unknown-unknown/release"
LEARN_TOKEN_WASM="$WASM_DIR/learn_token.optimized.wasm"
CREDENTIAL_NFT_WASM="$WASM_DIR/credential_nft.optimized.wasm"
PROGRESS_TRACKER_WASM="$WASM_DIR/progress_tracker.optimized.wasm"

calc_file_size() {
local file="$1"
if [ -f "$file" ]; then
stat -c%s "$file" 2>/dev/null || wc -c < "$file" | tr -d ' '
else
echo "0"
fi
}

calc_upload_cost() {
local size_bytes="$1"
# Soroban upload fee: ~0.0001 XLM base tx fee + storage rent deposit (~0.001 XLM per KB)
awk -v b="$size_bytes" 'BEGIN { printf "%.4f", 0.0001 + (b / 1024.0 * 0.001) }'
}

calc_deploy_cost() {
# Instance deployment creation fee and rent deposit: ~0.0500 XLM per contract
echo "0.0500"
}

calc_total_cost() {
local upload="$1"
local deploy="$2"
awk -v u="$upload" -v d="$deploy" 'BEGIN { printf "%.4f", u + d }'
}

LT_BYTES=$(calc_file_size "$LEARN_TOKEN_WASM")
CN_BYTES=$(calc_file_size "$CREDENTIAL_NFT_WASM")
PT_BYTES=$(calc_file_size "$PROGRESS_TRACKER_WASM")

LT_KB=$(awk -v b="$LT_BYTES" 'BEGIN { printf "%.1f", b / 1024.0 }')
CN_KB=$(awk -v b="$CN_BYTES" 'BEGIN { printf "%.1f", b / 1024.0 }')
PT_KB=$(awk -v b="$PT_BYTES" 'BEGIN { printf "%.1f", b / 1024.0 }')

LT_UPLOAD_COST=$(calc_upload_cost "$LT_BYTES")
CN_UPLOAD_COST=$(calc_upload_cost "$CN_BYTES")
PT_UPLOAD_COST=$(calc_upload_cost "$PT_BYTES")

LT_DEPLOY_COST=$(calc_deploy_cost)
CN_DEPLOY_COST=$(calc_deploy_cost)
PT_DEPLOY_COST=$(calc_deploy_cost)

LT_TOTAL_COST=$(calc_total_cost "$LT_UPLOAD_COST" "$LT_DEPLOY_COST")
CN_TOTAL_COST=$(calc_total_cost "$CN_UPLOAD_COST" "$CN_DEPLOY_COST")
PT_TOTAL_COST=$(calc_total_cost "$PT_UPLOAD_COST" "$PT_DEPLOY_COST")

GRAND_TOTAL_COST=$(awk -v a="$LT_TOTAL_COST" -v b="$CN_TOTAL_COST" -v c="$PT_TOTAL_COST" 'BEGIN { printf "%.4f", a + b + c }')

echo "=== Deployment Cost Estimation ==="
echo "Network: $NETWORK"
printf "%-18s %12s %18s %18s %16s\n" "Contract" "WASM Size" "WASM Upload (XLM)" "Deploy Cost (XLM)" "Total Est. (XLM)"
echo "------------------------------------------------------------------------------------------------"
printf "%-18s %10s KB %18s %18s %16s\n" "learn-token" "$LT_KB" "~$LT_UPLOAD_COST" "~$LT_DEPLOY_COST" "~$LT_TOTAL_COST"
printf "%-18s %10s KB %18s %18s %16s\n" "credential-nft" "$CN_KB" "~$CN_UPLOAD_COST" "~$CN_DEPLOY_COST" "~$CN_TOTAL_COST"
printf "%-18s %10s KB %18s %18s %16s\n" "progress-tracker" "$PT_KB" "~$PT_UPLOAD_COST" "~$PT_DEPLOY_COST" "~$PT_TOTAL_COST"
echo "------------------------------------------------------------------------------------------------"
echo "Total Estimated Deployment Fee: ~$GRAND_TOTAL_COST XLM"
echo "Recommended Account Balance: ~$(awk -v g="$GRAND_TOTAL_COST" 'BEGIN { printf "%.2f", g + 1.0 }') XLM (includes minimum reserve)"
echo ""

if [ "$ESTIMATE_ONLY" = true ]; then
echo "(--estimate-only specified: exiting without deploying)"
exit 0
fi

# Confirmation prompt if --confirm was not specified (#602)
if [ "$CONFIRM" = false ]; then
read -r -p "Do you want to proceed with deployment to $NETWORK? [y/N]: " PROCEED_INPUT
case "$PROCEED_INPUT" in
[yY]|[yY][eE][sS])
echo "Confirmation received. Proceeding with deployment..."
;;
*)
echo "Deployment cancelled by user."
exit 0
;;
esac
else
echo "(--confirm specified: proceeding without prompt)"
fi
echo ""

# On mainnet, perform safety checks. On testnet, skip for faster iteration (#61).
DEPLOY_FLAGS=()
Expand All @@ -82,37 +232,112 @@ else
DEPLOY_FLAGS+=(--ignore-checks)
fi

# ── State Tracking & Partial Failure Rollback Support (#603) ──────────────────

LEARN_TOKEN_ID=""
CREDENTIAL_NFT_ID=""
PROGRESS_TRACKER_ID=""

if [ "$RESUME" = true ] && [ -f "$PARTIAL_STATE_FILE" ]; then
echo "Resuming deployment from partial state in $PARTIAL_STATE_FILE..."
LEARN_TOKEN_ID=$(jq -r '.contracts.learn_token // empty' "$PARTIAL_STATE_FILE")
CREDENTIAL_NFT_ID=$(jq -r '.contracts.credential_nft // empty' "$PARTIAL_STATE_FILE")
PROGRESS_TRACKER_ID=$(jq -r '.contracts.progress_tracker // empty' "$PARTIAL_STATE_FILE")
fi

save_partial_state() {
cat > "$PARTIAL_STATE_FILE" << EOF
{
"network": "$NETWORK",
"updated_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
"contracts": {
"learn_token": "${LEARN_TOKEN_ID:-}",
"credential_nft": "${CREDENTIAL_NFT_ID:-}",
"progress_tracker": "${PROGRESS_TRACKER_ID:-}"
}
}
EOF
}

handle_deployment_failure() {
local exit_code="$?"
local failed_contract="$1"
echo ""
echo "================================================================================"
echo "DEPLOYMENT FAILED during $failed_contract deployment!"
echo "================================================================================"
echo "Partial deployment state saved to: $PARTIAL_STATE_FILE"
echo ""
echo "Deployed contracts so far:"
echo " - learn-token: ${LEARN_TOKEN_ID:-<not deployed>}"
echo " - credential-nft: ${CREDENTIAL_NFT_ID:-<not deployed>}"
echo " - progress-tracker: ${PROGRESS_TRACKER_ID:-<not deployed>}"
echo ""
echo "State & Rollback Notice:"
echo " Stellar contracts cannot be automatically deleted or rolled back on-chain."
echo " The contracts deployed before this failure remain valid on the network."
echo ""
echo "Next Steps:"
echo " 1. To resume deployment from this point once issues are resolved, run:"
echo " $0 $NETWORK --resume $([ "$CONFIRM" = true ] && echo "--confirm")"
echo " 2. To abandon this deployment and start clean, discard the partial state:"
echo " rm -f $PARTIAL_STATE_FILE"
echo "================================================================================"
exit "$exit_code"
}

# Deploy learn-token
echo "[2/4] Deploying learn-token..."
LEARN_TOKEN_ID=$(soroban contract deploy \
--wasm target/wasm32-unknown-unknown/release/learn_token.optimized.wasm \
--source "$STELLAR_SECRET_KEY" \
--rpc-url "$RPC_URL" \
--network-passphrase "$NETWORK_PASSPHRASE" \
"${DEPLOY_FLAGS[@]+"${DEPLOY_FLAGS[@]}"}")
echo " learn-token deployed: $LEARN_TOKEN_ID"
if [ -n "$LEARN_TOKEN_ID" ] && [[ "$LEARN_TOKEN_ID" =~ ^C[A-Z0-9]{55,62}$ ]]; then
echo " learn-token already deployed (resumed): $LEARN_TOKEN_ID"
else
trap 'handle_deployment_failure "learn-token"' ERR
LEARN_TOKEN_ID=$(soroban contract deploy \
--wasm "$LEARN_TOKEN_WASM" \
--source "$STELLAR_SECRET_KEY" \
--rpc-url "$RPC_URL" \
--network-passphrase "$NETWORK_PASSPHRASE" \
"${DEPLOY_FLAGS[@]+"${DEPLOY_FLAGS[@]}"}")
save_partial_state
echo " learn-token deployed: $LEARN_TOKEN_ID"
fi

# Deploy credential-nft
echo "[3/4] Deploying credential-nft..."
CREDENTIAL_NFT_ID=$(soroban contract deploy \
--wasm target/wasm32-unknown-unknown/release/credential_nft.optimized.wasm \
--source "$STELLAR_SECRET_KEY" \
--rpc-url "$RPC_URL" \
--network-passphrase "$NETWORK_PASSPHRASE" \
"${DEPLOY_FLAGS[@]+"${DEPLOY_FLAGS[@]}"}")
echo " credential-nft deployed: $CREDENTIAL_NFT_ID"
if [ -n "$CREDENTIAL_NFT_ID" ] && [[ "$CREDENTIAL_NFT_ID" =~ ^C[A-Z0-9]{55,62}$ ]]; then
echo " credential-nft already deployed (resumed): $CREDENTIAL_NFT_ID"
else
trap 'handle_deployment_failure "credential-nft"' ERR
CREDENTIAL_NFT_ID=$(soroban contract deploy \
--wasm "$CREDENTIAL_NFT_WASM" \
--source "$STELLAR_SECRET_KEY" \
--rpc-url "$RPC_URL" \
--network-passphrase "$NETWORK_PASSPHRASE" \
"${DEPLOY_FLAGS[@]+"${DEPLOY_FLAGS[@]}"}")
save_partial_state
echo " credential-nft deployed: $CREDENTIAL_NFT_ID"
fi

# Deploy progress-tracker
echo "[4/4] Deploying progress-tracker..."
PROGRESS_TRACKER_ID=$(soroban contract deploy \
--wasm target/wasm32-unknown-unknown/release/progress_tracker.optimized.wasm \
--source "$STELLAR_SECRET_KEY" \
--rpc-url "$RPC_URL" \
--network-passphrase "$NETWORK_PASSPHRASE" \
"${DEPLOY_FLAGS[@]+"${DEPLOY_FLAGS[@]}"}")
echo " progress-tracker deployed: $PROGRESS_TRACKER_ID"

# Write deployment info to file and validate (#60)
if [ -n "$PROGRESS_TRACKER_ID" ] && [[ "$PROGRESS_TRACKER_ID" =~ ^C[A-Z0-9]{55,62}$ ]]; then
echo " progress-tracker already deployed (resumed): $PROGRESS_TRACKER_ID"
else
trap 'handle_deployment_failure "progress-tracker"' ERR
PROGRESS_TRACKER_ID=$(soroban contract deploy \
--wasm "$PROGRESS_TRACKER_WASM" \
--source "$STELLAR_SECRET_KEY" \
--rpc-url "$RPC_URL" \
--network-passphrase "$NETWORK_PASSPHRASE" \
"${DEPLOY_FLAGS[@]+"${DEPLOY_FLAGS[@]}"}")
save_partial_state
echo " progress-tracker deployed: $PROGRESS_TRACKER_ID"
fi

# Clear error trap now that all contracts are deployed
trap - ERR

# Write final deployment info to file and validate (#60)
cat > "$DEPLOY_FILE" << EOF
{
"network": "$NETWORK",
Expand All @@ -125,6 +350,9 @@ cat > "$DEPLOY_FILE" << EOF
}
EOF

# Remove partial state file upon complete success (#603)
rm -f "$PARTIAL_STATE_FILE"

# Validate the written JSON is parseable
if ! jq empty "$DEPLOY_FILE" 2>/dev/null; then
echo "Error: Failed to write valid JSON to $DEPLOY_FILE"
Expand Down
Loading