From d8ca3dc5f3b2d5ffb99aaaf9fe6f3575d10d876a Mon Sep 17 00:00:00 2001 From: sallymoc Date: Sun, 14 Jun 2026 22:01:48 +0200 Subject: [PATCH 1/2] fix(smart_contracts): detect removed procedures in refresh script The merge logic was additive-only for procedures: methods removed upstream were never deleted from smart_contracts.json, so the refresh workflow's checksum never changed and no PR was opened. Now a script-generated procedure (one with a sourceIdentifier) that is absent from a successfully-fetched source is pruned. Manual procedures (no sourceIdentifier) are preserved, and pruning is guarded on fetch success so a transient GitHub outage cannot wipe procedures. Also log new contracts, new procedures, and removed procedures so the GitHub Actions run log shows what changed. Applying the fix removes qRWA methods 5 (CreateAssetReleasePoll) and 6 (VoteAssetRelease), which were removed from qRWA.h. --- data/smart_contracts.json | 10 ---------- scripts/update_smart_contracts.py | 26 ++++++++++++++++++++++---- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/data/smart_contracts.json b/data/smart_contracts.json index a06aed6..026d530 100644 --- a/data/smart_contracts.json +++ b/data/smart_contracts.json @@ -921,16 +921,6 @@ "name": "Vote Gov Params", "sourceIdentifier": "VoteGovParams" }, - { - "id": 5, - "name": "Create Asset Release Poll", - "sourceIdentifier": "CreateAssetReleasePoll" - }, - { - "id": 6, - "name": "Vote Asset Release", - "sourceIdentifier": "VoteAssetRelease" - }, { "id": 7, "name": "Deposit General Asset", diff --git a/scripts/update_smart_contracts.py b/scripts/update_smart_contracts.py index 53473df..233a61f 100644 --- a/scripts/update_smart_contracts.py +++ b/scripts/update_smart_contracts.py @@ -603,8 +603,14 @@ def merge_contracts( if not isinstance(fname, str): continue + # Transient flag (not persisted): whether the contract source was fetched + # successfully this run. Used to safely prune procedures removed upstream. + source_fetched = bool(new.pop("_sourceFetched", False)) + if fname not in by_filename: new["procedures"] = normalize_procs_to_list(new.get("procedures", [])) + print(f" {fname}: new contract added (index {new.get('contractIndex')}, " + f"{len(new['procedures'])} procedure(s))") existing.append(new) by_filename[fname] = new continue @@ -679,14 +685,24 @@ def merge_contracts( merged_procs.append(ex_p) else: # New procedure + print(f" {fname}: procedure id {pid} ({new_p.get('sourceIdentifier')}) " + f"added from source") merged_procs.append(new_p) seen_ids.add(pid) - # Keep any existing procedures that are no longer in the fresh list - # (in case they were manually added) + # Handle existing procedures that are no longer in the fresh list. for pid, p in ex_by_id.items(): - if pid not in seen_ids: - merged_procs.append(p) + if pid in seen_ids: + continue + # A script-generated procedure (has sourceIdentifier) that vanished from a + # successfully-fetched source was removed upstream -> drop it. Procedures + # without a sourceIdentifier are manual additions and are always preserved. + # Guard on source_fetched so a transient fetch failure can't wipe procedures. + if source_fetched and isinstance(p, dict) and p.get("sourceIdentifier"): + print(f" {fname}: procedure id {pid} ({p.get('sourceIdentifier')}) " + f"removed from source, dropping") + continue + merged_procs.append(p) merged_procs.sort(key=lambda x: x["id"]) ex["procedures"] = merged_procs @@ -797,6 +813,8 @@ def main(): "address": addr, "allowTransferShares": allow_transfer_shares, "procedures": procs, + # Transient (popped during merge): did we fetch the source this run? + "_sourceFetched": text is not None, } if construction_epoch is not None: From be2e4bf9ee1e370f17b97b2e5e977fa73cd1cdd0 Mon Sep 17 00:00:00 2001 From: sallymoc Date: Sun, 14 Jun 2026 22:35:10 +0200 Subject: [PATCH 2/2] fix(smart_contracts): guard procedure pruning against empty fetches Address code review: a 200 response with a truncated/garbage body parses to zero procedures, which would wipe a contract's entire procedure list. Only prune when the fresh fetch yielded at least one procedure (fresh_has_procs); individually removed procedures are still pruned. Also update the module docstring to describe the now-destructive merge behavior and the fetch-failure guard. --- scripts/update_smart_contracts.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/scripts/update_smart_contracts.py b/scripts/update_smart_contracts.py index 233a61f..76c5828 100644 --- a/scripts/update_smart_contracts.py +++ b/scripts/update_smart_contracts.py @@ -19,7 +19,11 @@ - address: computed via Node using your JS helper: const publicKey = helper.getIdentityBytes(addr56); // addr56 built from contractIndex (A..P, LE, len=56) const identity = await helper.getIdentity(publicKey) // 60-char with checksum -- Non-destructive merge: adds new contracts and new procedure IDs; preserves manual edits and custom fields. +- Merge: adds new contracts and new procedure IDs, and removes script-generated procedures + (those with a `sourceIdentifier`) that no longer exist in a successfully-fetched source. + Manual edits (procedures without `sourceIdentifier`) and custom fields are always preserved. + Procedure removal is suppressed for a contract whose source failed to fetch or fetched empty, + so a transient/garbage response can't wipe its procedures. Authoritative fields (auto-updated from GitHub): filename, name, contractIndex, address, githubUrl, firstUseEpoch, sharesAuctionEpoch, allowTransferShares, procedures. Preserved fields: label (if already set), and any custom fields (e.g., proposalUrl, etc.). @@ -691,14 +695,20 @@ def merge_contracts( seen_ids.add(pid) # Handle existing procedures that are no longer in the fresh list. + # Only prune when the fetch yielded at least one procedure. A 200 response + # with a truncated/garbage body parses to zero procedures, which would + # otherwise wipe a contract's entire procedure list (N -> 0); requiring a + # non-empty fresh list still prunes individually removed procedures. + fresh_has_procs = len(new_list) > 0 for pid, p in ex_by_id.items(): if pid in seen_ids: continue # A script-generated procedure (has sourceIdentifier) that vanished from a # successfully-fetched source was removed upstream -> drop it. Procedures # without a sourceIdentifier are manual additions and are always preserved. - # Guard on source_fetched so a transient fetch failure can't wipe procedures. - if source_fetched and isinstance(p, dict) and p.get("sourceIdentifier"): + # Guard on source_fetched + fresh_has_procs so a transient fetch failure + # or a bad-but-200 response can't wipe procedures. + if source_fetched and fresh_has_procs and isinstance(p, dict) and p.get("sourceIdentifier"): print(f" {fname}: procedure id {pid} ({p.get('sourceIdentifier')}) " f"removed from source, dropping") continue