From 882a9da6ffcf253f83306494255de870f217e89f Mon Sep 17 00:00:00 2001 From: Bogdan Melnyk Date: Tue, 11 Aug 2026 08:15:18 +0200 Subject: [PATCH] fix(airdrop): scope tiering to RustChain merged PRs --- node/airdrop_v2.py | 15 ++++++------ node/test_airdrop_v2.py | 54 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/node/airdrop_v2.py b/node/airdrop_v2.py index 5052a8376..cf695cbdf 100644 --- a/node/airdrop_v2.py +++ b/node/airdrop_v2.py @@ -639,16 +639,15 @@ def _determine_tier( if user_resp.status_code != 200: return None - # Get contributions (PRs merged) - # Use GitHub search API for contributions + # Count merged PRs scoped to the RustChain org, not global GitHub activity. contrib_resp = requests.get( - f"https://api.github.com/search/commits", - headers={ - **headers, - "Accept": "application/vnd.github.cloak-preview", - }, + "https://api.github.com/search/issues", + headers=headers, params={ - "q": f"author:{github_username} merged:true", + "q": ( + f"author:{github_username} org:Scottcjn " + "is:pr is:merged" + ), "per_page": 1, }, timeout=10, diff --git a/node/test_airdrop_v2.py b/node/test_airdrop_v2.py index 3072a09bb..5d2dce04a 100644 --- a/node/test_airdrop_v2.py +++ b/node/test_airdrop_v2.py @@ -141,7 +141,11 @@ def side_effect(url, *args, **kwargs): return mock_user elif "starred" in url: return mock_stars - elif "search/commits" in url: + elif "search/issues" in url: + self.assertEqual( + kwargs["params"]["q"], + "author:testuser org:Scottcjn is:pr is:merged", + ) return mock_contrib return Mock(status_code=404) @@ -155,11 +159,55 @@ def side_effect(url, *args, **kwargs): skip_antisybil=True, # Skip wallet checks, but still determine tier from GitHub ) - # With mock returning 3 PRs, user should be eligible for Builder tier + # With mock returning 3 org-scoped merged PRs, user should be eligible for Builder tier self.assertTrue(result.eligible) - self.assertEqual(result.tier, "builder") # 3 PRs = Builder tier + self.assertEqual(result.tier, "builder") # 3 merged PRs in Scottcjn org = Builder tier self.assertEqual(result.reward_uwrtc, 100 * 1_000_000) + @patch("requests.get") + def test_tier_search_ignores_global_commit_history(self, mock_get): + """Tiering must use org-scoped merged PRs, not global commit counts.""" + mock_user = Mock(status_code=200) + mock_user.json.return_value = { + "login": "octocat", + "created_at": "2020-01-01T00:00:00Z", + } + mock_user.headers = {} + + mock_contrib = Mock(status_code=200) + mock_contrib.json.return_value = {"total_count": 0} + + mock_stars = Mock(status_code=200) + mock_stars.headers = { + "Link": '; rel="last"' + } + mock_stars.json.return_value = [] + + seen = [] + + def side_effect(url, *args, **kwargs): + seen.append((url, kwargs.get("params"))) + if url.endswith("/users/octocat"): + return mock_user + if url.endswith("/search/issues"): + return mock_contrib + if url.endswith("/users/octocat/starred"): + return mock_stars + return Mock(status_code=404) + + mock_get.side_effect = side_effect + + tier = self.airdrop._determine_tier("octocat") + + self.assertEqual(tier, EligibilityTier.STARGAZER) + self.assertIn( + ( + "https://api.github.com/search/issues", + {"q": "author:octocat org:Scottcjn is:pr is:merged", "per_page": 1}, + ), + seen, + ) + def test_invalid_chain(self): """Test eligibility check with invalid chain.""" result = self.airdrop.check_eligibility(