diff --git a/stellar/contracts/wormhole-contract/src/tests/run-integration-tests.sh b/stellar/contracts/wormhole-contract/src/tests/run-integration-tests.sh new file mode 100755 index 00000000000..ccc239d0a80 --- /dev/null +++ b/stellar/contracts/wormhole-contract/src/tests/run-integration-tests.sh @@ -0,0 +1,194 @@ +#!/usr/bin/env bash +# ============================================================================= +# Wormhole Core Integration Test Suite for Stellar +# ============================================================================= +# +# This script deploys the Wormhole Core contract to Stellar testnet and runs +# a comprehensive suite of integration tests to verify contract functionality. +# +# PREREQUISITES: +# - stellar CLI installed and configured +# - jq installed for JSON parsing +# - curl installed for RPC calls +# - Test data files in ./test_data/ directory: +# * guardian_keys.json - Initial guardian keys (3 guardians) +# * guardian_set_upgrade_vaas.json - VAAs for guardian set transitions +# * set_message_fee_vaas.json - VAAs for fee configuration +# * transfer_fees_testnet_vaas.json - VAAs for fee transfers +# - Network access to Stellar testnet +# +# USAGE: +# ./run-integration-tests.sh [network] +# +# Arguments: +# network Target network (default: testnet, only testnet supported) +# +# ENVIRONMENT VARIABLES: +# DEPLOYER_IDENTITY Stellar identity name for deployment (default: deployer) +# +# TEST FLOW: +# 1. Deploy fresh Wormhole Core contract via deploy.sh +# 2. Verify initial guardian set index is 0 +# 3. Upgrade guardian set: 0 → 1 → 2 +# 4. Set message fee to 10 XLM +# 5. Fund contract with XLM and test fee transfers (0.5 XLM) +# 6. Post message with fee (user1) +# 7. Reset fee to zero and post message without fee (user2) +# +# OUTPUTS: +# - Contract info written to $STELLAR_ROOT/.testnet-contract-info +# - Prints "ok: " on success +# +# EXIT CODES: +# 0 - All tests passed +# 1 - Test failure or missing dependency +# +# ============================================================================= +set -euo pipefail + +die() { echo "run-integration-tests.sh: $*" >&2; exit 1; } +need() { command -v "$1" >/dev/null 2>&1 || die "missing '$1'"; } +step() { echo "==> $*" >&2; } + +# ----------------------------------------------------------------------------- +# Configuration +# ----------------------------------------------------------------------------- +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +STELLAR_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +TEST_DATA="$SCRIPT_DIR/test_data" +DEPLOY_SH="$STELLAR_ROOT/scripts/deploy.sh" +NETWORK="${1:-testnet}" +DEPLOYER_IDENTITY="${DEPLOYER_IDENTITY:-deployer}" + +# ----------------------------------------------------------------------------- +# Validation +# ----------------------------------------------------------------------------- +[[ "$NETWORK" == "testnet" ]] || die "only testnet is supported (got: $NETWORK)" +[[ -d "$TEST_DATA" ]] || die "missing test data: $TEST_DATA" +[[ -x "$DEPLOY_SH" ]] || die "missing deploy script: $DEPLOY_SH" + +need stellar +need jq +need curl + +cd "$STELLAR_ROOT" + +# ----------------------------------------------------------------------------- +# Deploy Contract +# ----------------------------------------------------------------------------- +step "deploy Wormhole Core" +DEPLOY_JSON="$("$DEPLOY_SH" "$NETWORK")" +CONTRACT_ID="$(jq -er '.contract_id' <<<"$DEPLOY_JSON")" +XLM_CONTRACT="$(stellar contract id asset --asset native --network "$NETWORK")" + +# ----------------------------------------------------------------------------- +# Helper Functions +# ----------------------------------------------------------------------------- + +# Extract VAA hex from test data JSON by test case name +# Usage: vaa_hex +vaa_hex() { jq -er --arg name "$2" '.testCases[] | select(.name==$name) | .vaa.hex' "$1"; } + +# Invoke Wormhole Core contract method +# Usage: core [args...] +core() { local src="$1"; shift; stellar contract invoke --id "$CONTRACT_ID" --source-account "$src" --network "$NETWORK" -- "$@"; } + +# Invoke native XLM contract method (for transfers/approvals) +# Usage: xlm [args...] +xlm() { local src="$1"; shift; stellar contract invoke --id "$XLM_CONTRACT" --source-account "$src" --network "$NETWORK" -- "$@"; } + +# Ensure a Stellar identity exists and is funded +# Creates the identity if it doesn't exist, funds via friendbot +# Usage: ensure_identity +# Returns: The identity's public address +ensure_identity() { + local name="$1" + stellar keys address "$name" >/dev/null 2>&1 || stellar keys generate "$name" --network "$NETWORK" >/dev/null 2>&1 + stellar keys fund "$name" --network "$NETWORK" >/dev/null 2>&1 || true + stellar keys address "$name" +} + +# ----------------------------------------------------------------------------- +# Setup Test Identities +# ----------------------------------------------------------------------------- +USER1_ADDR="$(ensure_identity user1)" +USER2_ADDR="$(ensure_identity user2)" +DEPLOYER_ADDR="$(stellar keys address "$DEPLOYER_IDENTITY")" + +# ============================================================================= +# TEST EXECUTION +# ============================================================================= + +# Test 1: Verify contract initialization +step "verify init" +[[ "$(core "$DEPLOYER_IDENTITY" get_current_guardian_set_index | grep -Eo '[0-9]+' | head -1)" == "0" ]] \ + || die "unexpected guardian set index" + +# Test 2: Guardian set upgrade chain (0 → 1 → 2) +step "guardian set upgrades" +core "$DEPLOYER_IDENTITY" submit_guardian_set_upgrade \ + --vaa-bytes "\"$(vaa_hex "$TEST_DATA/guardian_set_upgrade_vaas.json" guardian_set_upgrade_0_to_1)\"" >/dev/null +core "$DEPLOYER_IDENTITY" submit_guardian_set_upgrade \ + --vaa-bytes "\"$(vaa_hex "$TEST_DATA/guardian_set_upgrade_vaas.json" guardian_set_upgrade_1_to_2)\"" >/dev/null +[[ "$(core "$DEPLOYER_IDENTITY" get_current_guardian_set_index | grep -Eo '[0-9]+' | head -1)" == "2" ]] \ + || die "guardian set upgrade failed" + +# Test 3: Set message fee via governance VAA +step "set message fee (10 XLM)" +core "$DEPLOYER_IDENTITY" submit_set_message_fee \ + --vaa-bytes "\"$(vaa_hex "$TEST_DATA/set_message_fee_vaas.json" set_message_fee_10_xlm)\"" >/dev/null +[[ "$(core "$DEPLOYER_IDENTITY" get_message_fee | grep -Eo '[0-9]+' | head -1)" == "100000000" ]] \ + || die "unexpected message fee" + +# Test 4: Fund contract and verify fee transfer governance action +step "fund contract and transfer fees" +xlm "$DEPLOYER_IDENTITY" transfer --from "$DEPLOYER_ADDR" --to "$CONTRACT_ID" --amount 200000000 >/dev/null +BALANCE_BEFORE="$(xlm "$DEPLOYER_IDENTITY" balance --id "$CONTRACT_ID" | grep -Eo '[0-9]+' | head -1)" +core "$DEPLOYER_IDENTITY" submit_transfer_fees \ + --vaa-bytes "\"$(vaa_hex "$TEST_DATA/transfer_fees_testnet_vaas.json" transfer_fees_0.5_xlm)\"" >/dev/null +BALANCE_AFTER="$(xlm "$DEPLOYER_IDENTITY" balance --id "$CONTRACT_ID" | grep -Eo '[0-9]+' | head -1)" +[[ "$BALANCE_AFTER" == "$((BALANCE_BEFORE - 5000000))" ]] || die "fee transfer balance mismatch" + +# Test 5: Post a message with fee payment (requires XLM approval) +step "post message (with fee)" +# Use stellar rpc to get current ledger, then add offset for expiration +# Max offset allowed is 3110400 (~180 days), we use 1M (~58 days) +LEDGER_JSON=$(curl -s -X POST "https://soroban-testnet.stellar.org" \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"getLatestLedger"}' 2>/dev/null || echo '{}') +CURRENT_LEDGER=$(echo "$LEDGER_JSON" | jq -r '.result.sequence // 4000000') +EXPIRATION_LEDGER=$((CURRENT_LEDGER + 1000000)) +xlm user1 approve --from "$USER1_ADDR" --spender "$CONTRACT_ID" --amount 100000000 --expiration-ledger "$EXPIRATION_LEDGER" >/dev/null +core user1 post_message \ + --emitter "$USER1_ADDR" \ + --nonce 42 \ + --payload "48656c6c6f20576f726d686f6c6521" \ + --consistency-level 1 >/dev/null # 1 = Confirmed, payload = "Hello Wormhole!" + +# Test 6: Post a message without fee (after setting fee to zero) +step "post message (no fee)" +core "$DEPLOYER_IDENTITY" submit_set_message_fee \ + --vaa-bytes "\"$(vaa_hex "$TEST_DATA/set_message_fee_vaas.json" set_message_fee_zero_fee)\"" >/dev/null +core user2 post_message \ + --emitter "$USER2_ADDR" \ + --nonce 100 \ + --payload "5465737420776974686f757420666565" \ + --consistency-level 1 >/dev/null # 1 = Confirmed, payload = "Test without fee" + +# ----------------------------------------------------------------------------- +# Save Contract Info for Subsequent Scripts +# ----------------------------------------------------------------------------- +cat > "$STELLAR_ROOT/.testnet-contract-info" < [--yes] + +set -euo pipefail + +# Helper functions +die() { echo "deploy.sh: $*" >&2; exit 1; } +need() { command -v "$1" >/dev/null 2>&1 || die "missing '$1'"; } +step() { echo "==> $*" >&2; } +usage() { + cat >&2 < [OPTIONS] + +Deploy the Stellar Wormhole contract to the specified network. + +Arguments: + testnet|mainnet Target network for deployment + +Options: + --yes Skip confirmation prompt (required for mainnet deployments) + -h, --help Show this help message + +Requirements: + - stellar CLI tool must be installed and in PATH + - yq must be installed (YAML processor) + - curl must be installed + - Configuration file must exist at: $CONFIG_DIR/.yaml + +Configuration: + The script reads configuration from YAML files in $CONFIG_DIR/: + - testnet.yaml: Configuration for testnet deployment + - mainnet.yaml: Configuration for mainnet deployment + + Required fields in config: + - deployer: Stellar keys identity name + - friendbot_url: URL for funding testnet accounts (can be empty for mainnet) + - governance_emitter: 32-byte hex address of the governance emitter + (standard is 0000...0004) + - guardians: Array of guardian addresses used by the contract constructor + +Examples: + # Deploy to testnet + $(basename "$0") testnet + + # Deploy to mainnet (requires --yes flag) + $(basename "$0") mainnet --yes + +Output: + The script outputs deployment information as JSON, including: + - network: Target network name + - contract_id: Deployed contract ID + - wasm_hash: Hash of the deployed WASM + - deployer: Deployer account address + - guardian_count: Number of guardians initialized + - initialized: Whether constructor initialization was performed + - timestamp: Deployment timestamp (UTC) +EOF + exit 2 +} + +# Set up directory paths +# SCRIPT_DIR: absolute path to the directory containing this script +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# STELLAR_ROOT: absolute path to the stellar directory (parent of scripts/) +STELLAR_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +CONFIG_DIR="$SCRIPT_DIR/config" + +# Parse command-line arguments +# NETWORK: first positional argument (testnet or mainnet) +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage +fi + +NETWORK="${1:-}"; shift || true +YES=false + +# Process remaining command-line flags +while [[ $# -gt 0 ]]; do + case "$1" in + --yes) YES=true ;; # Skip confirmation prompt (required for mainnet) + -h|--help) usage ;; + *) die "unknown option: $1" ;; + esac + shift +done + +# Validate inputs and check dependencies +[[ -n "$NETWORK" ]] || usage +need stellar # Stellar CLI tool +need yq # YAML processor +need curl # HTTP client + +# Load configuration from YAML file +CONFIG_FILE="$CONFIG_DIR/$NETWORK.yaml" +[[ -f "$CONFIG_FILE" ]] || die "unknown network '$NETWORK' (missing $CONFIG_FILE)" + +# Extract configuration values from YAML +DEPLOYER="$(yq -r '.deployer' "$CONFIG_FILE")" # Stellar keys identity name +FRIENDBOT_URL="$(yq -r '.friendbot_url' "$CONFIG_FILE")" # Friendbot URL for funding (testnet only) +GUARDIANS_JSON="$(yq -o=json '.guardians' "$CONFIG_FILE")" # Guardian addresses as JSON array +GUARDIAN_COUNT="$(yq -r '.guardians | length' "$CONFIG_FILE")" # Number of guardians +GOVERNANCE_EMITTER="$(yq -r '.governance_emitter' "$CONFIG_FILE")" # Governance emitter address (32 bytes hex) + +# Validate configuration +[[ -n "$DEPLOYER" && "$DEPLOYER" != "null" ]] || die "config missing .deployer: $CONFIG_FILE" +[[ -n "$GOVERNANCE_EMITTER" && "$GOVERNANCE_EMITTER" != "null" ]] || die "config missing .governance_emitter: $CONFIG_FILE" +[[ "$GUARDIAN_COUNT" -gt 0 ]] || die "config has no guardians" +# Require --yes flag for mainnet deployments +[[ "$NETWORK" != "mainnet" || "$YES" == "true" ]] || die "mainnet requires --yes" + +# Ensure deployer key exists, generate if needed +stellar keys address "$DEPLOYER" >/dev/null 2>&1 || stellar keys generate "$DEPLOYER" --network "$NETWORK" >/dev/null 2>&1 +DEPLOYER_ADDR="$(stellar keys address "$DEPLOYER")" + +# Fund deployer account using friendbot (testnet only) +if [[ -n "$FRIENDBOT_URL" && "$FRIENDBOT_URL" != "null" ]]; then + step "fund deployer (friendbot)" + curl -sSf --max-time 30 "$FRIENDBOT_URL?addr=$DEPLOYER_ADDR" >/dev/null 2>&1 || true + sleep 2 +fi + +# Set up contract paths +CONTRACT_DIR="$STELLAR_ROOT/contracts/wormhole-contract" +CONTRACT_WASM="$STELLAR_ROOT/target/wasm32v1-none/release/wormhole_contract.wasm" + +# Build the contract +step "build" +(cd "$CONTRACT_DIR" && stellar contract build --optimize >/dev/null) +[[ -f "$CONTRACT_WASM" ]] || die "missing wasm: $CONTRACT_WASM" + +# Deploy the contract +step "deploy and initialize ($NETWORK, $GUARDIAN_COUNT guardians)" +DEPLOY_OUTPUT="$(stellar contract deploy \ + --wasm "$CONTRACT_WASM" \ + --source-account "$DEPLOYER" \ + --network "$NETWORK" \ + -- \ + --initial_guardians "$GUARDIANS_JSON" \ + --governance_emitter "$GOVERNANCE_EMITTER" 2>&1)" +# Extract contract ID from deployment output (Stellar contract IDs are 56 chars starting with 'C') +CONTRACT_ID="$(grep -Eo 'C[A-Z0-9]{55}' <<<"$DEPLOY_OUTPUT" | head -1 || true)" +[[ -n "$CONTRACT_ID" ]] || { echo "$DEPLOY_OUTPUT" >&2; die "failed to parse contract id"; } +# Extract WASM hash from deployment output (64 hex characters) +WASM_HASH="$(grep -Eo '[a-f0-9]{64}' <<<"$DEPLOY_OUTPUT" | head -1 || true)" + +# Verify constructor initialization succeeded by checking guardian set index is 0. +[[ "$(stellar contract invoke --id "$CONTRACT_ID" --source-account "$DEPLOYER" --network "$NETWORK" -- \ + get_current_guardian_set_index 2>/dev/null | grep -Eo '[0-9]+' | head -1)" == "0" ]] \ + || die "constructor init failed" + +# Output deployment summary +TIMESTAMP="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" +INITIALIZED="yes" + +# Human-readable output to stderr (for display) +{ + echo "" + echo "┌─────────────────────────────────────────────────────────────────┐" + echo "│ ✅ DEPLOYMENT SUCCESSFUL │" + echo "└─────────────────────────────────────────────────────────────────┘" + echo "" + echo " Network: $NETWORK" + echo " Contract ID: $CONTRACT_ID" + echo " WASM Hash: ${WASM_HASH:-N/A}" + echo " Deployer: $DEPLOYER_ADDR" + echo " Guardians: $GUARDIAN_COUNT" + echo " Initialized: $INITIALIZED" + echo " Timestamp: $TIMESTAMP" + echo "" +} >&2 + +# JSON output to stdout (for scripting) +printf '{"network":"%s","contract_id":"%s","wasm_hash":"%s","deployer":"%s","guardian_count":%s,"initialized":%s,"timestamp":"%s"}\n' \ + "$NETWORK" "$CONTRACT_ID" "${WASM_HASH:-}" "$DEPLOYER_ADDR" "$GUARDIAN_COUNT" \ + true "$TIMESTAMP" diff --git a/stellar/stellar.yaml b/stellar/stellar.yaml new file mode 100644 index 00000000000..42d3c25c527 --- /dev/null +++ b/stellar/stellar.yaml @@ -0,0 +1,47 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: stellar + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: stellar + template: + metadata: + labels: + app: stellar + spec: + containers: + - name: stellar + image: stellar/quickstart:latest + command: ["/start"] + args: ["--local", "--enable-soroban-rpc"] + ports: + - containerPort: 8000 + name: rpc + readinessProbe: + exec: + command: + - /bin/sh + - -c + - "curl -X POST http://localhost:8000/soroban/rpc -H 'Content-Type: application/json' -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getLatestLedger\"}' | grep -q 'sequence'" + initialDelaySeconds: 45 + periodSeconds: 5 + failureThreshold: 20 +--- +apiVersion: v1 +kind: Service +metadata: + name: stellar +spec: + selector: + app: stellar + ports: + - name: rpc + port: 8000 + targetPort: 8000 + - name: horizon + port: 8001 + targetPort: 8001 \ No newline at end of file