Skip to content

all issues fixed

all issues fixed #274

Workflow file for this run

name: Smart Contract - Build & CI
on:
push:
branches: [ main ]
tags: [ 'v*' ]
pull_request:
branches: [ main ]
jobs:
ci:
name: Contract CI
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
targets: wasm32-unknown-unknown
- name: Install cargo-audit and cargo-deny
run: |
cargo install cargo-audit --locked
cargo install cargo-deny --locked
- name: Run cargo audit
run: |
cargo audit
- name: Run cargo deny
run: |
cargo deny check
- name: Install stellar-cli
uses: stellar/stellar-cli@v25.0.0
- name: Build smart contracts
run: |
stellar contract build --release
- name: Run recurring-payment tests
run: cargo test -- recurring -- --nocapture
- name: Generate wasm-manifest.json
run: |
python3 <<'PY'
import glob
import hashlib
import json
import os
import re
from datetime import datetime, timezone
wasm_files = sorted(glob.glob("target/wasm32*/release/*.wasm"))
if not wasm_files:
raise SystemExit("No WASM files found after contract build")
# Prefer the Accord contract artifact when multiple WASMs exist.
wasm_path = next(
(p for p in wasm_files if os.path.basename(p) == "accord.wasm"),
wasm_files[0],
)
with open(wasm_path, "rb") as f:
wasm_hash = hashlib.sha256(f.read()).hexdigest()
cargo_toml = open("Cargo.toml", encoding="utf-8").read()
match = re.search(
r'^soroban-sdk\s*=\s*"=?([^"\n]+)"',
cargo_toml,
flags=re.MULTILINE,
)
if not match:
raise SystemExit("Could not read soroban-sdk version from Cargo.toml")
soroban_sdk_version = match.group(1)
# Probed: True if contract source includes recurring-payment types/entrypoints
try:
lib_rs = open("contracts/accord/src/lib.rs", encoding="utf-8").read()
supports_recurring_payments = "RecurringPayment" in lib_rs and "disburse_recurring" in lib_rs
except FileNotFoundError:
supports_recurring_payments = False
manifest = {
"wasm_file": os.path.basename(wasm_path),
"wasm_hash": wasm_hash,
"commit_sha": os.environ.get("GITHUB_SHA", ""),
"build_timestamp": datetime.now(timezone.utc)
.replace(microsecond=0)
.isoformat()
.replace("+00:00", "Z"),
"soroban_sdk_version": soroban_sdk_version,
"supports_recurring_payments": supports_recurring_payments,
}
with open("wasm-manifest.json", "w", encoding="utf-8") as out:
json.dump(manifest, out, indent=2)
out.write("\n")
print(json.dumps(manifest, indent=2))
PY
- name: Upload compiled WASM artifact
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
uses: actions/upload-artifact@v4
with:
name: accord-contract-wasm
path: |
target/wasm32-unknown-unknown/release/*.wasm
wasm-manifest.json
retention-days: 90
- name: Report WASM file sizes
run: |
python3 -c '
import os, glob
files = glob.glob("target/wasm32*/release/*.wasm")
if not files:
print("No WASM files found in release target directories.")
for f in files:
if os.path.isfile(f):
size_bytes = os.path.getsize(f)
size_kb = size_bytes / 1024.0
print(f"Contract: {os.path.basename(f)} | Size: {size_kb:.2f} KB ({size_bytes} bytes)")
'
# Runs only on push to main — never on pull requests.
# Requires TESTNET_CONTRACT_ID to be set as a repository variable under
# Settings → Secrets and variables → Actions → Variables.
# When the variable is absent the job logs a message and exits cleanly so
# forks and contributor PRs are unaffected.
testnet-smoke-test:
name: Testnet Smoke Test
runs-on: ubuntu-latest
needs: ci
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Check TESTNET_CONTRACT_ID is configured
id: guard
run: |
if [ -z "${{ vars.TESTNET_CONTRACT_ID }}" ]; then
echo "TESTNET_CONTRACT_ID repository variable is not set — skipping smoke test."
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Install Stellar CLI
if: steps.guard.outputs.skip != 'true'
uses: stellar/stellar-cli@v25.0.0
- name: Simulate get_total_proposals on deployed testnet contract
if: steps.guard.outputs.skip != 'true'
id: invoke
run: |
result=$(stellar contract invoke \
--network testnet \
--id "${{ vars.TESTNET_CONTRACT_ID }}" \
-- get_total_proposals)
echo "result=$result" >> "$GITHUB_OUTPUT"
- name: Report live proposal count
if: steps.guard.outputs.skip != 'true'
run: |
echo "Deployed contract get_total_proposals returned: ${{ steps.invoke.outputs.result }}"
# Pre-mainnet deployment gate (issue #420).
# Runs on version tags (v*) that gate mainnet deployment. Queries the live
# testnet contract and fails if sum(owner weights) != get_total_weight.
# Skips cleanly when TESTNET_CONTRACT_ID is unset (same pattern as smoke test).
weight-invariant-gate:
name: Weight Invariant Gate
runs-on: ubuntu-latest
needs: ci
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
steps:
- name: Check TESTNET_CONTRACT_ID is configured
id: guard
run: |
if [ -z "${{ vars.TESTNET_CONTRACT_ID }}" ]; then
echo "TESTNET_CONTRACT_ID repository variable is not set — skipping weight invariant gate."
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Install Stellar CLI
if: steps.guard.outputs.skip != 'true'
uses: stellar/stellar-cli@v25.0.0
- name: Verify total-weight invariant on testnet
if: steps.guard.outputs.skip != 'true'
env:
CONTRACT_ID: ${{ vars.TESTNET_CONTRACT_ID }}
run: |
set -euo pipefail
weights_raw=$(stellar contract invoke \
--network testnet \
--id "$CONTRACT_ID" \
-- get_owner_weights)
total_raw=$(stellar contract invoke \
--network testnet \
--id "$CONTRACT_ID" \
-- get_total_weight)
export WEIGHTS_RAW="$weights_raw"
export TOTAL_RAW="$total_raw"
python3 <<'PY'
import json, os, re, sys
def parse_u32(value):
if isinstance(value, bool):
raise TypeError(value)
if isinstance(value, int):
return value
if isinstance(value, str):
return int(value, 10)
raise TypeError(type(value))
def extract_weight(entry):
if isinstance(entry, dict):
for key, val in entry.items():
if str(key).lower() == "weight":
return parse_u32(val)
raise ValueError(f"Cannot read weight from entry: {entry!r}")
weights_raw = os.environ["WEIGHTS_RAW"].strip()
total_raw = os.environ["TOTAL_RAW"].strip()
try:
weights = json.loads(weights_raw)
except json.JSONDecodeError:
found = re.findall(r'"?weight"?\s*[:=]\s*"?(\d+)"?', weights_raw, flags=re.I)
if not found:
print("Failed to parse get_owner_weights output:", weights_raw, file=sys.stderr)
sys.exit(1)
summed = sum(int(w) for w in found)
else:
if not isinstance(weights, list):
print("Expected a list from get_owner_weights, got:", type(weights), file=sys.stderr)
sys.exit(1)
summed = sum(extract_weight(entry) for entry in weights)
try:
reported = parse_u32(json.loads(total_raw))
except Exception:
m = re.search(r"\d+", total_raw)
if not m:
print("Failed to parse get_total_weight output:", total_raw, file=sys.stderr)
sys.exit(1)
reported = int(m.group(0))
print(f"sum(owner weights) = {summed}")
print(f"get_total_weight = {reported}")
if summed != reported:
print(
"Weight invariant failed: sum of owner weights does not equal reported total weight.",
file=sys.stderr,
)
sys.exit(1)
print("Weight invariant OK.")
PY