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
16 changes: 11 additions & 5 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
50 changes: 47 additions & 3 deletions backend/src/config/env.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' })
Expand Down
34 changes: 30 additions & 4 deletions backend/src/soroban/soroban.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand All @@ -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<string>(envVar, '');
// Try the primary env var first (with SOROBAN_ prefix)
let fromEnv = this.configService.get<string>(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<string>(legacyEnvVar, '');
if (fromEnv && fromEnv.length > 10) return fromEnv;

// Legacy single-contract env var (backward compat)
const legacy = this.configService.get<string>('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<string>('SOROBAN_CONTRACT_ID', '');
if (singleLegacy && singleLegacy.length > 10 && contractName === 'inventory') return singleLegacy;

try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
Expand Down
144 changes: 125 additions & 19 deletions lifebank-soroban/scripts/deploy-testnet.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 \
Expand All @@ -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"

Expand All @@ -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"
Expand Down
Loading