diff --git a/backend/.env.example b/backend/.env.example index ffc6463a..4ee0df39 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -48,13 +48,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 8b7e97c5..efcec020 100644 --- a/backend/src/config/env.schema.ts +++ b/backend/src/config/env.schema.ts @@ -129,15 +129,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 20c7b5bb..5db03aa4 100644 --- a/backend/src/soroban/soroban.service.ts +++ b/backend/src/soroban/soroban.service.ts @@ -97,6 +97,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, @@ -176,11 +184,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(); @@ -193,15 +212,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 diff --git a/lifebank-soroban/scripts/deploy-testnet.sh b/lifebank-soroban/scripts/deploy-testnet.sh index 53ebfcb4..ed3d0a98 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 stellar CLI is installed @@ -21,13 +24,103 @@ 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 -for contract in coordinator identity inventory payments requests temperature matching reputation delivery analytics; do +on_deploy_error() { + local exit_code=$? + echo "" + 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 + 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 +} + +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..." CONTRACT_ID=$(stellar contract deploy \ @@ -38,11 +131,28 @@ 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}" + + # 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 -# Save contract IDs to testnet-specific file -echo "💾 Saving contract IDs to .contract-ids.testnet.json..." +# 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..." OUTPUT_FILE=".contract-ids.testnet.json" @@ -59,27 +169,23 @@ done echo "" echo "✅ Deployment complete!" echo "" -echo "📝 Contract IDs saved to ${OUTPUT_FILE}" - -echo "" -echo "🔧 Next step: Initialize contracts" -echo " Before using the contracts, you must run the initialization script:" -echo "" -echo " STELLAR_IDENTITY=${IDENTITY} STELLAR_NETWORK=${NETWORK} ./scripts/initialize-contracts.sh" -echo "" -echo " This initializes each contract with the admin address and dependency references." -echo "" +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"