From 90a57de40a50634a70b9c9d62ad853e27208b633 Mon Sep 17 00:00:00 2001 From: alfred micheal Date: Mon, 29 Jun 2026 16:37:11 +0100 Subject: [PATCH 1/3] feat: add per-contract env vars for all 10 soroban contracts (#1022) --- backend/.env.example | 16 ++++++--- backend/src/config/env.schema.ts | 50 ++++++++++++++++++++++++-- backend/src/soroban/soroban.service.ts | 34 +++++++++++++++--- 3 files changed, 88 insertions(+), 12 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index 5325fb1a..b40374e5 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -45,13 +45,19 @@ SOROBAN_CONFIRMATION_DEPTH=1 EXPECTED_CONTRACT_VERSION=1 # Per-contract addresses (populated by scripts/generate-bindings.sh after deployment). +# All 10 contracts: coordinator, identity, inventory, payments, requests, temperature, matching, reputation, delivery, analytics. # These take precedence over the addresses in lifebank-soroban/contracts.json. # Leave blank to fall back to contracts.json for the active SOROBAN_NETWORK. -COORDINATOR_CONTRACT_ID= -INVENTORY_CONTRACT_ID= -PAYMENTS_CONTRACT_ID= -REQUESTS_CONTRACT_ID= -TEMPERATURE_CONTRACT_ID= +SOROBAN_COORDINATOR_CONTRACT_ID= +SOROBAN_IDENTITY_CONTRACT_ID= +SOROBAN_INVENTORY_CONTRACT_ID= +SOROBAN_PAYMENTS_CONTRACT_ID= +SOROBAN_REQUESTS_CONTRACT_ID= +SOROBAN_TEMPERATURE_CONTRACT_ID= +SOROBAN_MATCHING_CONTRACT_ID= +SOROBAN_REPUTATION_CONTRACT_ID= +SOROBAN_DELIVERY_CONTRACT_ID= +SOROBAN_ANALYTICS_CONTRACT_ID= # Legacy single-contract ID (kept for backward compatibility — prefer the per-contract vars above). SOROBAN_CONTRACT_ID= diff --git a/backend/src/config/env.schema.ts b/backend/src/config/env.schema.ts index 36d7c944..4d07161b 100644 --- a/backend/src/config/env.schema.ts +++ b/backend/src/config/env.schema.ts @@ -127,15 +127,59 @@ export class EnvironmentVariables { @IsNotEmpty({ message: 'MAPS_API_KEY is required' }) MAPS_API_KEY: string; - // ─── Soroban Blockchain ─────────────────────────────────────────────────── + // ─── Soroban Blockchain ─────────────────────────────────────────────── @IsUrl({}, { message: 'SOROBAN_RPC_URL must be a valid URL' }) @IsNotEmpty({ message: 'SOROBAN_RPC_URL is required' }) SOROBAN_RPC_URL: string; + // ─── Per-contract addresses (all 10 deployed contracts) ───────────────── + + @IsOptional() + @IsString() + SOROBAN_COORDINATOR_CONTRACT_ID: string = ''; + + @IsOptional() + @IsString() + SOROBAN_IDENTITY_CONTRACT_ID: string = ''; + + @IsOptional() + @IsString() + SOROBAN_INVENTORY_CONTRACT_ID: string = ''; + + @IsOptional() + @IsString() + SOROBAN_PAYMENTS_CONTRACT_ID: string = ''; + + @IsOptional() + @IsString() + SOROBAN_REQUESTS_CONTRACT_ID: string = ''; + + @IsOptional() + @IsString() + SOROBAN_TEMPERATURE_CONTRACT_ID: string = ''; + + @IsOptional() + @IsString() + SOROBAN_MATCHING_CONTRACT_ID: string = ''; + + @IsOptional() + @IsString() + SOROBAN_REPUTATION_CONTRACT_ID: string = ''; + + @IsOptional() + @IsString() + SOROBAN_DELIVERY_CONTRACT_ID: string = ''; + + @IsOptional() + @IsString() + SOROBAN_ANALYTICS_CONTRACT_ID: string = ''; + + // ─── Legacy contract ID (kept for backward compatibility) ────────────── + + @IsOptional() @IsString() - @IsNotEmpty({ message: 'SOROBAN_CONTRACT_ID is required' }) - SOROBAN_CONTRACT_ID: string; + SOROBAN_CONTRACT_ID: string = ''; @IsString() @IsNotEmpty({ message: 'SOROBAN_SECRET_KEY is required' }) diff --git a/backend/src/soroban/soroban.service.ts b/backend/src/soroban/soroban.service.ts index 82e5736a..f1e224bc 100644 --- a/backend/src/soroban/soroban.service.ts +++ b/backend/src/soroban/soroban.service.ts @@ -89,6 +89,14 @@ export class SorobanService implements OnModuleInit { private requestsClient: RequestsClient | null = null; private temperatureClient: TemperatureClient | null = null; + // ── Additional contract clients (SDKs not yet generated) ─────────────────── + // These will be initialized once their SDKs are available: + // - identityClient (SOROBAN_IDENTITY_CONTRACT_ID) + // - matchingClient (SOROBAN_MATCHING_CONTRACT_ID) + // - reputationClient (SOROBAN_REPUTATION_CONTRACT_ID) + // - deliveryClient (SOROBAN_DELIVERY_CONTRACT_ID) + // - analyticsClient (SOROBAN_ANALYTICS_CONTRACT_ID) + private readonly retryConfig: RetryConfig = { maxRetries: 3, initialDelay: 1000, @@ -154,11 +162,22 @@ export class SorobanService implements OnModuleInit { this.temperatureClient = new TemperatureClient({ contractId: temperatureId, ...sharedOptions }); } + // Resolve additional contract IDs (SDKs to be generated in future sprints) + const identityId = this.resolveContractId('SOROBAN_IDENTITY_CONTRACT_ID', 'identity'); + const matchingId = this.resolveContractId('SOROBAN_MATCHING_CONTRACT_ID', 'matching'); + const reputationId = this.resolveContractId('SOROBAN_REPUTATION_CONTRACT_ID', 'reputation'); + const deliveryId = this.resolveContractId('SOROBAN_DELIVERY_CONTRACT_ID', 'delivery'); + const analyticsId = this.resolveContractId('SOROBAN_ANALYTICS_CONTRACT_ID', 'analytics'); + this.logger.log(`Soroban service initialized on ${network}`); this.logger.log( `Clients ready: inventory=${!!this.inventoryClient}, coordinator=${!!this.coordinatorClient}, ` + `payments=${!!this.paymentsClient}, requests=${!!this.requestsClient}, temperature=${!!this.temperatureClient}`, ); + this.logger.log( + `Additional contracts resolved: identity=${!!identityId}, matching=${!!matchingId}, ` + + `reputation=${!!reputationId}, delivery=${!!deliveryId}, analytics=${!!analyticsId} (SDKs pending)`, + ); try { await this.validateContractCompatibility(); @@ -171,15 +190,22 @@ export class SorobanService implements OnModuleInit { /** * Resolve a contract ID from env var, falling back to contracts.json. + * Supports both SOROBAN_* and legacy *_CONTRACT_ID naming conventions. * Returns an empty string if neither source has a real address. */ private resolveContractId(envVar: string, contractName: string): string { - const fromEnv = this.configService.get(envVar, ''); + // Try the primary env var first (with SOROBAN_ prefix) + let fromEnv = this.configService.get(envVar, ''); + if (fromEnv && fromEnv.length > 10) return fromEnv; + + // Fall back to legacy naming convention for backward compatibility (without SOROBAN_ prefix) + const legacyEnvVar = `${contractName.toUpperCase()}_CONTRACT_ID`; + fromEnv = this.configService.get(legacyEnvVar, ''); if (fromEnv && fromEnv.length > 10) return fromEnv; - // Legacy single-contract env var (backward compat) - const legacy = this.configService.get('SOROBAN_CONTRACT_ID', ''); - if (legacy && legacy.length > 10 && contractName === 'inventory') return legacy; + // Legacy single-contract env var (backward compat) — only for inventory + const singleLegacy = this.configService.get('SOROBAN_CONTRACT_ID', ''); + if (singleLegacy && singleLegacy.length > 10 && contractName === 'inventory') return singleLegacy; try { // eslint-disable-next-line @typescript-eslint/no-var-requires From 6893c8bf4d429a644f8fd0388d999801b4a74c0c Mon Sep 17 00:00:00 2001 From: alfred micheal Date: Mon, 29 Jun 2026 17:35:36 +0100 Subject: [PATCH 2/3] feat: add rollback mechanism and dry-run validation to deploy script (#1023) --- lifebank-soroban/scripts/deploy-testnet.sh | 98 ++++++++++++++++++++-- 1 file changed, 89 insertions(+), 9 deletions(-) diff --git a/lifebank-soroban/scripts/deploy-testnet.sh b/lifebank-soroban/scripts/deploy-testnet.sh index 576b5be8..3d6f5cb5 100755 --- a/lifebank-soroban/scripts/deploy-testnet.sh +++ b/lifebank-soroban/scripts/deploy-testnet.sh @@ -5,8 +5,11 @@ set -e # Configuration NETWORK="testnet" IDENTITY=${STELLAR_IDENTITY:-default} # Override via STELLAR_IDENTITY env var; falls back to "default" for local dev +DRY_RUN="${1:-false}" # Pass --dry-run as first argument to validate without deploying +CONTRACT_IDS_FILE=".contract-ids.json" echo "🚀 Deploying Lifebank contracts to ${NETWORK}..." +[[ "$DRY_RUN" == "--dry-run" ]] && echo "ℹ️ DRY RUN MODE — will validate without deploying" echo "" # Check if soroban CLI is installed @@ -21,13 +24,74 @@ echo "📦 Building contracts..." ./scripts/build-all.sh echo "" + +# ── Validation phase: Ensure all WASMs exist ───────────────────────────────────── +echo "✓ Validating WASM binaries..." +echo "" + +CONTRACTS_TO_DEPLOY=(coordinator identity inventory payments requests temperature matching reputation delivery analytics) + +for contract in "${CONTRACTS_TO_DEPLOY[@]}"; do + WASM_FILE="target/wasm32-unknown-unknown/release/${contract}_contract.wasm" + if [[ ! -f "$WASM_FILE" ]]; then + echo "❌ Error: Missing WASM binary for $contract" + echo " Expected: $WASM_FILE" + exit 1 + fi + echo " ✓ ${contract}: $WASM_FILE" +done + +echo "" + +# Exit early if dry-run mode +if [[ "$DRY_RUN" == "--dry-run" ]]; then + echo "✅ Dry-run validation successful! All WASM binaries are present." + echo " Run without --dry-run to deploy contracts." + exit 0 +fi + +# ── Deployment phase ───────────────────────────────────────────────────────────── echo "🌐 Deploying to ${NETWORK}..." echo "" -# Deployment order: coordinator first (it's a dependency for other contracts) +# Initialize contract IDs tracking file with partial status +{ + jq -n '{ + status: "partial", + network: "'${NETWORK}'", + deployed_at: "'$(date -Iseconds)'", + deployments: {} + }' > "${CONTRACT_IDS_FILE}" +} + declare -A CONTRACT_IDS +DEPLOYED_COUNT=0 + +# Set trap to log state on error +trap 'on_deploy_error' ERR + +on_deploy_error() { + local exit_code=$? + echo "" + echo "❌ Deployment failed!" + echo "" + echo "Partially deployed contracts ($DEPLOYED_COUNT of ${#CONTRACTS_TO_DEPLOY[@]}):" + for contract in "${CONTRACTS_TO_DEPLOY[@]:0:$DEPLOYED_COUNT}"; do + echo " - $contract: ${CONTRACT_IDS[$contract]}" + done + echo "" + echo "State saved to: $CONTRACT_IDS_FILE" + echo "" + echo "To clean up, manually delete these contracts from the network:" + echo " for id in $(jq -r '.deployments | keys[]' \"$CONTRACT_IDS_FILE\"); do" + echo " soroban contract invoke --id \$id --source ${IDENTITY} --network ${NETWORK} -- version" + echo " done" + echo "" + exit $exit_code +} -for contract in coordinator identity inventory payments requests temperature matching reputation delivery analytics; do +# Deploy each contract +for contract in "${CONTRACTS_TO_DEPLOY[@]}"; do echo "Deploying ${contract} contract..." CONTRACT_ID=$(soroban contract deploy \ @@ -38,10 +102,21 @@ for contract in coordinator identity inventory payments requests temperature mat CONTRACT_IDS[$contract]=$CONTRACT_ID echo " ✅ ${contract}: ${CONTRACT_ID}" + + # Save incrementally to file with contract ID and status + jq --arg contract "$contract" --arg id "$CONTRACT_ID" \ + '.deployments[$contract] = $id' "${CONTRACT_IDS_FILE}" > "${CONTRACT_IDS_FILE}.tmp" + mv "${CONTRACT_IDS_FILE}.tmp" "${CONTRACT_IDS_FILE}" + + ((DEPLOYED_COUNT++)) echo "" done -# Update contracts.json with deployed IDs +# Mark deployment as complete +jq '.status = "complete"' "${CONTRACT_IDS_FILE}" > "${CONTRACT_IDS_FILE}.tmp" +mv "${CONTRACT_IDS_FILE}.tmp" "${CONTRACT_IDS_FILE}" + +# ── Update contracts.json ──────────────────────────────────────────────────────── echo "💾 Updating contracts.json with deployed IDs..." { @@ -61,18 +136,23 @@ echo "💾 Updating contracts.json with deployed IDs..." echo "" echo "✅ Deployment complete!" echo "" -echo "📝 Contract IDs saved to .contract-ids.json" +echo "📝 Contract IDs saved to $CONTRACT_IDS_FILE and contracts.json" # ── Regenerate TypeScript bindings (issue #846) ──────────────────────────────── echo "" echo "🔗 Regenerating TypeScript client bindings..." # Export contract IDs so generate-bindings.sh can pick them up -export COORDINATOR_CONTRACT_ID="${CONTRACT_IDS[coordinator]:-}" -export INVENTORY_CONTRACT_ID="${CONTRACT_IDS[inventory]}" -export PAYMENTS_CONTRACT_ID="${CONTRACT_IDS[payments]}" -export REQUESTS_CONTRACT_ID="${CONTRACT_IDS[requests]}" -export TEMPERATURE_CONTRACT_ID="${CONTRACT_IDS[temperature]:-}" +export SOROBAN_COORDINATOR_CONTRACT_ID="${CONTRACT_IDS[coordinator]:-}" +export SOROBAN_IDENTITY_CONTRACT_ID="${CONTRACT_IDS[identity]:-}" +export SOROBAN_INVENTORY_CONTRACT_ID="${CONTRACT_IDS[inventory]:-}" +export SOROBAN_PAYMENTS_CONTRACT_ID="${CONTRACT_IDS[payments]:-}" +export SOROBAN_REQUESTS_CONTRACT_ID="${CONTRACT_IDS[requests]:-}" +export SOROBAN_TEMPERATURE_CONTRACT_ID="${CONTRACT_IDS[temperature]:-}" +export SOROBAN_MATCHING_CONTRACT_ID="${CONTRACT_IDS[matching]:-}" +export SOROBAN_REPUTATION_CONTRACT_ID="${CONTRACT_IDS[reputation]:-}" +export SOROBAN_DELIVERY_CONTRACT_ID="${CONTRACT_IDS[delivery]:-}" +export SOROBAN_ANALYTICS_CONTRACT_ID="${CONTRACT_IDS[analytics]:-}" export SOROBAN_NETWORK="${NETWORK}" GENERATE_SCRIPT="$(cd "$(dirname "$0")/../.." && pwd)/scripts/generate-bindings.sh" From 5bd13c60676f95c1efe4ba260b8f525db0ce67ea Mon Sep 17 00:00:00 2001 From: alfred micheal Date: Mon, 29 Jun 2026 17:39:35 +0100 Subject: [PATCH 3/3] feat: add post-deployment verification to ensure contracts are live on-chain (#1024) --- lifebank-soroban/scripts/deploy-testnet.sh | 37 +++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/lifebank-soroban/scripts/deploy-testnet.sh b/lifebank-soroban/scripts/deploy-testnet.sh index 3d6f5cb5..ccafbb5e 100755 --- a/lifebank-soroban/scripts/deploy-testnet.sh +++ b/lifebank-soroban/scripts/deploy-testnet.sh @@ -73,7 +73,7 @@ trap 'on_deploy_error' ERR on_deploy_error() { local exit_code=$? echo "" - echo "❌ Deployment failed!" + echo "❌ Deployment or verification failed!" echo "" echo "Partially deployed contracts ($DEPLOYED_COUNT of ${#CONTRACTS_TO_DEPLOY[@]}):" for contract in "${CONTRACTS_TO_DEPLOY[@]:0:$DEPLOYED_COUNT}"; do @@ -90,6 +90,35 @@ on_deploy_error() { exit $exit_code } +verify_contract_deployed() { + local contract=$1 + local contract_id=$2 + local max_retries=5 + local attempt=0 + + echo " Verifying ${contract} is live on-chain..." + + while [[ $attempt -lt $max_retries ]]; do + if soroban contract invoke \ + --id "$contract_id" \ + --source ${IDENTITY} \ + --network ${NETWORK} \ + -- version > /dev/null 2>&1; then + echo " ✓ ${contract} verified on-chain" + return 0 + fi + + ((attempt++)) + if [[ $attempt -lt $max_retries ]]; then + echo " ⟳ Retrying... (attempt $attempt/$max_retries)" + sleep 2 + fi + done + + echo " ✗ ${contract} verification failed after $max_retries attempts" + return 1 +} + # Deploy each contract for contract in "${CONTRACTS_TO_DEPLOY[@]}"; do echo "Deploying ${contract} contract..." @@ -108,6 +137,12 @@ for contract in "${CONTRACTS_TO_DEPLOY[@]}"; do '.deployments[$contract] = $id' "${CONTRACT_IDS_FILE}" > "${CONTRACT_IDS_FILE}.tmp" mv "${CONTRACT_IDS_FILE}.tmp" "${CONTRACT_IDS_FILE}" + # Verify the contract is actually reachable on-chain + if ! verify_contract_deployed "$contract" "$CONTRACT_ID"; then + echo "❌ Contract verification failed — aborting deployment" + exit 1 + fi + ((DEPLOYED_COUNT++)) echo "" done