Skip to content
88 changes: 88 additions & 0 deletions .github/workflows/refresh-smart-contracts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: Refresh Smart Contracts

on:
schedule:
# Every Wednesday at 14:00 UTC
- cron: '0 14 * * 3'
workflow_dispatch: # Allow manual trigger

jobs:
refresh-smart-contracts:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: dev
fetch-depth: 0

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.x'

- name: Install Python dependencies
run: pip install requests

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Get checksum before update
id: before
run: |
echo "checksum=$(sha256sum data/smart_contracts.json | cut -d ' ' -f 1)" >> $GITHUB_OUTPUT

- name: Run update script
run: python3 scripts/update_smart_contracts.py

- name: Get checksum after update
id: after
run: |
echo "checksum=$(sha256sum data/smart_contracts.json | cut -d ' ' -f 1)" >> $GITHUB_OUTPUT

- name: Check for changes
id: changes
run: |
if [ "${{ steps.before.outputs.checksum }}" != "${{ steps.after.outputs.checksum }}" ]; then
echo "has_changes=true" >> $GITHUB_OUTPUT
else
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "No changes detected in smart_contracts.json"
fi

- name: Create branch and PR
if: steps.changes.outputs.has_changes == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
BRANCH_NAME="chore/refresh-smart-contracts-$(date +%Y%m%d)"

git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

git checkout -b "$BRANCH_NAME"
git add data/smart_contracts.json
git commit -m "chore(smart_contracts): refresh smart contracts data"
git push origin "$BRANCH_NAME"

gh pr create \
--base dev \
--head "$BRANCH_NAME" \
--title "chore(smart_contracts): refresh smart contracts data" \
--body "$(cat <<'EOF'
## Summary
- Automated weekly refresh of smart contracts data
- Generated by scheduled workflow

## Changes
Updated \`data/smart_contracts.json\` with latest contract data from GitHub.

---
🤖 This PR was automatically created by the refresh-smart-contracts workflow.
EOF
)"
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
# [1.11.0-rc.1](https://github.com/qubic/static/compare/v1.10.0...v1.11.0-rc.1) (2025-12-27)


### Bug Fixes

* **smart_contracts:** fail script if current epoch fetch fails ([eb361fc](https://github.com/qubic/static/commit/eb361fc92811d44302a38c593d73b54ab23091e6))


### Features

* **ci:** add weekly smart contracts refresh workflow ([7a301b2](https://github.com/qubic/static/commit/7a301b2c92e020608b9dc7d51a87c509c04e1711))
* **smart_contracts:** ignore contracts with future constructionEpoch on refresh ([835eb31](https://github.com/qubic/static/commit/835eb31c0d298664d7e05c4d3efa9e4463f201ee))

# [1.10.0](https://github.com/qubic/static/compare/v1.9.0...v1.10.0) (2025-12-17)


Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "qubic-static-api",
"version": "1.10.0",
"version": "1.11.0-rc.1",
"description": "Static data and assets for Qubic blockchain",
"private": true,
"packageManager": "pnpm@9.11.0",
Expand Down
50 changes: 43 additions & 7 deletions scripts/update_smart_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@

RAW_BASE_CONTRACTS = "https://raw.githubusercontent.com/qubic/core/main/src/contracts/"
RAW_CONTRACT_DEF = "https://raw.githubusercontent.com/qubic/core/main/src/contract_core/contract_def.h"
QUBIC_STATS_API = "https://rpc.qubic.org/v1/latest-stats"

# ---------------------------- Regexes ---------------------------------------

Expand Down Expand Up @@ -148,6 +149,18 @@ def fetch_text(url: str) -> Optional[str]:
print(f"Warning: GET {url} failed: {e}")
return None

def fetch_current_epoch() -> Optional[int]:
"""Fetch the current epoch from the Qubic stats API."""
try:
resp = requests.get(QUBIC_STATS_API, timeout=30)
if resp.status_code == 200:
data = resp.json()
return data.get("data", {}).get("epoch")
print(f"Warning: GET {QUBIC_STATS_API} -> {resp.status_code}")
except Exception as e:
print(f"Warning: Failed to fetch current epoch: {e}")
return None

# ---------------------------- Parse contract_def.h --------------------------

def parse_contract_def_from_raw(raw_text: str, known_contract_basenames: Optional[set] = None) -> Dict[str, int]:
Expand All @@ -171,7 +184,12 @@ def parse_contract_def_from_raw(raw_text: str, known_contract_basenames: Optiona
mapping[basename] = cidx
return mapping

def extract_contract_names_from_descriptions(raw_text: str) -> Dict[int, str]:
def extract_contract_names_from_descriptions(raw_text: str) -> Dict[int, Dict[str, Any]]:
"""
Extract contract info from contractDescriptions array.
Each entry is like: {"QX", 66, 10000, sizeof(QX)}
Returns: {contractIndex: {"name": str, "constructionEpoch": int}}
"""
text = strip_comments(raw_text)
token = "contractDescriptions"
pos = text.find(token)
Expand Down Expand Up @@ -222,16 +240,20 @@ def extract_contract_names_from_descriptions(raw_text: str) -> Dict[int, str]:
break
i += 1

names: Dict[int, str] = {}
# Regex to parse: {"NAME", constructionEpoch, destructionEpoch, sizeof(...)}
item_re = re.compile(r'"([^"]+)"\s*,\s*(\d+)')

contracts: Dict[int, Dict[str, Any]] = {}
for idx1, item in enumerate(items, start=0):
if idx1 == 0:
continue
m = FIRST_QUOTED_STRING_RE.search(item)
m = item_re.search(item)
if not m:
continue
name = m.group(1)
names[idx1] = name
return names
construction_epoch = int(m.group(2))
contracts[idx1] = {"name": name, "constructionEpoch": construction_epoch}
return contracts

# ---------------------------- Header scanning -------------------------------

Expand Down Expand Up @@ -449,20 +471,34 @@ def main():
if not contract_def_text:
raise SystemExit("Could not fetch contract_def.h")

# Fetch current epoch to filter contracts
current_epoch = fetch_current_epoch()
if current_epoch is None:
raise SystemExit("Could not fetch current epoch from API")
print(f"Current epoch: {current_epoch}")

stripped = strip_comments(contract_def_text)

all_basenames = set(Path(m.group("path")).name for m in INCLUDE_RE.finditer(stripped))
basenames = {b for b in all_basenames if not should_skip_filename(b)}
idx_map = parse_contract_def_from_raw(stripped, basenames)

idx_to_name = extract_contract_names_from_descriptions(stripped)
idx_to_info = extract_contract_names_from_descriptions(stripped)

fresh_entries: List[Dict[str, Any]] = []
for basename in sorted(basenames):
cidx = idx_map.get(basename)
if cidx is None:
continue

# Skip contracts with constructionEpoch > currentEpoch
contract_info = idx_to_info.get(cidx, {})
construction_epoch = contract_info.get("constructionEpoch")
if current_epoch is not None and construction_epoch is not None:
if construction_epoch > current_epoch:
print(f"Skipping {basename}: constructionEpoch {construction_epoch} > current epoch {current_epoch}")
continue

url = RAW_BASE_CONTRACTS + basename
text = fetch_text(url)

Expand All @@ -482,7 +518,7 @@ def main():
stem = Path(basename).stem
label = label_from_filename_with_q_rule(stem)

name_value = idx_to_name.get(cidx, stem.upper())
name_value = contract_info.get("name", stem.upper())

addr: Optional[str] = None
identity = run_js_get_identity_from_index(cidx, js_lib_path)
Expand Down